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.