Skip to main content

Feature Store

Define features once and serve them consistently for training and inference. These endpoints define entities, feature views, and feature services, ingest and refresh values, resolve online features for serving, and generate point-in-time correct training data.

Feature stores are created in the MLOps user interface, where the offline Postgres and optional online Redis storage is bound. These endpoints operate on a store you already have access to.

All endpoints require authentication via the X-API-Key header. Reading needs the mlops:read scope; defining, writing, pushing, materializing and deprecating need mlops:write. Every request is authorized against your access to the target store; discovering a store does not imply permission to use it.


GET /api/v1/mlops/feature-stores​

Scope: mlops:read

List the feature stores your key can use, most recently updated first. It pages with limit, cursor and sort (Pagination).

Response

{
"data": [
{
"_id": "store_abc123",
"name": "driver_features",
"status": "ready",
"onlineEnabled": true
}
],
"meta": { "total": 1, "limit": 50, "nextCursor": null, "requestId": "req_abc123" }
}

GET /api/v1/mlops/feature-stores/:id​

Scope: mlops:read

Retrieve a single feature store with its definitions: entities, every version of its feature views (newest first per view, each with its status: active or deprecated), and its feature services. Read these before applying a change.


POST /api/v1/mlops/feature-stores/:id/apply​

Scope: mlops:write

Register or update entities, feature views, and feature services. Objects are applied in dependency order (entities, then views, then services). Applying is idempotent: an unchanged view is a no-op, an additive change updates in place, and a breaking change creates a new version. Send only the object kinds you want to change.

Request

{
"entities": [
{ "name": "driver", "joinKeys": ["driver_id"], "valueType": "int64" }
],
"views": [
{
"name": "driver_stats",
"entities": ["driver"],
"mode": "batch",
"ttl": "2d",
"schema": [
{ "name": "conv_rate", "dtype": "float64" },
{ "name": "acc_rate", "dtype": "float64" }
],
"source": {
"serviceKind": "addon",
"serviceId": "pg_addon_1",
"type": "postgres",
"table": "driver_stats_raw",
"timestampField": "event_timestamp",
"entityKeyColumns": { "driver_id": "driver_id" }
}
}
],
"services": [
{
"name": "driver_model_v1",
"features": [
{ "view": "driver_stats", "feature": "conv_rate" },
{ "view": "driver_stats", "feature": "acc_rate" }
]
}
]
}

Response

{
"data": {
"entities": ["ent_1"],
"views": [{ "viewId": "view_1", "version": 1, "change": "created" }],
"services": [{ "serviceId": "svc_1", "version": 1 }]
},
"meta": { "requestId": "req_abc123" }
}

Each view's change says what the apply did to it: created (its first version), identical (nothing changed), additive (updated in place, same version) or breaking (a new version was created).


POST /api/v1/mlops/feature-stores/:storeId/views/:name/rows​

Scope: mlops:write

Ingest feature rows into a view's history. Each row carries the entity join keys, an event_timestamp, and the view's feature columns.

Request

{
"rows": [
{ "driver_id": 1001, "event_timestamp": "2026-07-30T00:00:00Z", "conv_rate": 0.75, "acc_rate": 0.9 }
]
}

Response

{
"data": { "view": "driver_stats", "version": 1, "written": 1 },
"meta": { "requestId": "req_abc123" }
}

POST /api/v1/mlops/feature-stores/:storeId/views/:name/materialize​

Scope: mlops:write

Run the incremental refresh that publishes a view's latest values to the online store. For a batch view with a source table, the new rows in the source are pulled into the view's history first, so a materialize right after apply publishes the source's values without waiting for the scheduled pull. Only changed values are moved, and an older value never overwrites a newer one.

No body: the store and view are in the path.

Response

{
"data": { "view": "driver_stats", "version": 1, "ingested": 1, "materialized": 1, "watermark": "2026-07-30T00:00:00Z" },
"meta": { "requestId": "req_abc123" }
}

ingested is the number of source rows pulled first, or null when the view has no source table.


POST /api/v1/mlops/feature-stores/:id/online-features​

Scope: mlops:read

Resolve the latest online feature values for a feature service. Send only the entity keys per row. Each result reports the resolved features and lists any that were missing; absent data is always reported, never fabricated. Set maxStalenessSeconds to treat a value older than your tolerance as missing rather than serving it stale.

Request

{
"featureService": "driver_model_v1",
"entityRows": [{ "driver_id": 1001 }],
"maxStalenessSeconds": 600
}

Response

{
"data": {
"featureService": "driver_model_v1",
"version": 1,
"results": [
{ "keys": { "driver_id": 1001 }, "features": { "conv_rate": 0.75, "acc_rate": 0.9 }, "missing": [] }
]
},
"meta": { "requestId": "req_abc123" }
}

POST /api/v1/mlops/feature-stores/:id/historical-features​

Scope: mlops:read

Generate leak-free, point-in-time training data: for each labeled row, features are resolved as of that row's event_timestamp. Supply the entity and label frame exactly one way: entityRows (inline) or entityS3Uri (an s3:// URI or bare key of a Parquet or CSV frame, the bulk path for training-scale sets). Set entityFormat to parquet or csv if it cannot be inferred from the key.

The result is delivered as a Parquet snapshot you read directly. A view with no rows in its history yet contributes empty (null) feature values rather than an error.

Request (inline)

{
"featureService": "driver_model_v1",
"entityRows": [
{ "driver_id": 1001, "event_timestamp": "2026-07-30T12:00:00Z", "label": 1 },
{ "driver_id": 1002, "event_timestamp": "2026-07-30T12:00:00Z", "label": 0 }
]
}

Request (bulk)

{
"featureService": "driver_model_v1",
"entityS3Uri": "s3://your-bucket/labels/train.parquet"
}

Response

{
"data": {
"storeId": "store_abc123",
"featureService": "driver_model_v1",
"version": 1,
"features": [
{ "view": "driver_stats", "feature": "conv_rate", "viewVersion": 1 },
{ "view": "driver_stats", "feature": "acc_rate", "viewVersion": 1 }
],
"numRows": 2,
"snapshotKey": "feature-store-snapshots/store_abc123/driver_model_v1/9f2c.parquet",
"snapshotUrl": "https://s3.amazonaws.com/...&X-Amz-Signature=..."
},
"meta": { "requestId": "req_abc123" }
}

POST /api/v1/mlops/feature-stores/:storeId/views/:name/events​

Scope: mlops:write

Push real-time events to a streaming feature view with windowed aggregations. Each event carries the entity join keys, an event_timestamp and the columns the view aggregates; the online and offline aggregates are updated as the events arrive.

Request

{
"events": [
{ "driver_id": 1001, "event_timestamp": "2026-10-08T05:00:00Z", "trip_fare": 18.5 }
]
}

Response

{
"data": { "view": "driver_trips_stream", "version": 1, "published": 1 },
"meta": { "requestId": "req_abc123" }
}

POST /api/v1/mlops/feature-stores/:storeId/views/:name/versions/:version/deprecate​

Scope: mlops:write

Deprecate a version of a feature view, typically the older one after a breaking change created a new version. A deprecated version is read-only and hidden from discovery, but it is kept and the feature services pinned to it still resolve it.

No body: the store, view and version are in the path.

Response

{
"data": { "view": "driver_stats", "version": 1, "status": "deprecated" },
"meta": { "requestId": "req_abc123" }
}

404 when the store has no such view version; 400 when version is not an integer.