API
Snapshot History
Snapshots are materialized facet histograms for a namespace. They carry
facet listings in values[].v and facet counts in values[].n, stored
durably in S3 and mirrored into Aerospike for the latest body.
Use POST /snapshots to materialize a field now. Use history and body
routes to read the durable chronology written by the consistency watcher.
Snapshot policy
Configure automatic snapshot writes on the namespace’s Index CR:
apiVersion: hevlayer.com/v1
kind: Index
metadata:
name: products
spec:
backend:
namespace: products
snapshot:
interval: 5m
retention: 30d
facetFields:
- category
- brand
| Field | Default | Behavior |
|---|---|---|
facetFields | [] | Facet fields to histogram. Empty or unset disables the automatic snapshot writer for the namespace, so history and activity stay empty. |
interval | 5m | Minimum spacing between automatic snapshot writes. The writer fires on each upstream-stable advance; interval only floors how often a write lands. The gateway fallback is LAYER_SNAPSHOT_MIN_INTERVAL_MS. |
retention | never | never keeps all history. A duration such as 30d prunes S3 bodies older than the window, while always keeping the most recent body. |
Snapshots are event-driven, not scheduled: an idle namespace does not get a
new snapshot just because interval elapsed. The gateway refreshes Index
policy periodically, so edits take effect without a pod restart.
Manual POST /snapshots jobs with source: origin and the automatic
writer use the same shard fan-out path. Origin work is bounded by
spec.scan.threads; stored and cache snapshot reads do not fan out.
Routes
| Route | Method | Behavior |
|---|---|---|
POST /v2/namespaces/{ns}/snapshots | POST | Create an on-demand snapshot job for one field. |
GET /v2/namespaces/{ns}/snapshot-jobs | GET | List in-memory snapshot jobs. |
GET /v2/namespaces/{ns}/snapshot-jobs/{id} | GET | Read one snapshot job. |
GET /v2/namespaces/{ns}/history | GET | Newest-first durable snapshot history. |
GET /v2/namespaces/{ns}/snapshots/{sha} | GET | Full snapshot body by full SHA or 7-char prefix. |
GET /v2/activity/snapshots | GET | Cross-namespace snapshot-write activity stream. |
Manual snapshot
job = await client.create_snapshot("products", {
"field": "category",
"source": "auto",
"filters": ["brand", "Eq", "Acme"],
"page_size": 1000,
})job, err := client.CreateSnapshot(ctx, "products", &hevlayer.CreateSnapshotRequest{
Field: "category",
Source: "auto",
Filters: []interface{}{"brand", "Eq", "Acme"},
PageSize: 1000,
})const job = await client.createSnapshot("products", {
field: "category",
source: "auto",
filters: ["brand", "Eq", "Acme"],
page_size: 1000,
});curl -X POST "$LAYER_GATEWAY_URL/v2/namespaces/products/snapshots" \
-H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"field": "category",
"source": "auto",
"filters": ["brand", "Eq", "Acme"],
"page_size": 1000
}' Valid sources are auto, stored, cache, and origin.
| Source | Reads from | Notes |
|---|---|---|
auto | Stored snapshot when possible, otherwise cache/origin policy | Default. Stored snapshots only support unfiltered configured fields. |
stored | Latest S3 snapshot body, with Aerospike mirror as a cache | Fastest path for configured facet fields. |
cache | Aerospike document cache | Supports filters the cache can evaluate. |
origin | Turbopuffer paginated scan | Authoritative. Persists the computed snapshot body to S3. |
The response is 202 Accepted:
{
"id": "snapshot-job-uuid",
"namespace": "products",
"field": "category",
"source": "auto",
"status": "running",
"progress": 0,
"documents_scanned": 0,
"created_at": "2026-05-26T10:00:00Z"
}
Poll the job:
job = await client.get_snapshot_job("products", job.id)job, err := client.GetSnapshotJob(ctx, "products", jobID)const job = await client.getSnapshotJob("products", jobId);curl "$LAYER_GATEWAY_URL/v2/namespaces/products/snapshot-jobs/snapshot-job-uuid" \
-H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" Completed jobs include sha when a body was materialized:
{
"id": "snapshot-job-uuid",
"namespace": "products",
"field": "category",
"source": "origin",
"status": "completed",
"documents_scanned": 12844,
"sha": "3f9e8b21",
"stable_as_of": 1747300000123
}
History
history = await client.list_namespace_history("products", limit=20)history, err := client.ListNamespaceHistory(ctx, "products",
&hevlayer.ListNamespaceHistoryParams{Limit: 20})const history = await client.listNamespaceHistory("products", { limit: 20 });curl "$LAYER_GATEWAY_URL/v2/namespaces/products/history?limit=20" \
-H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" [
{"watermark_ms": 1747300000123, "sha": "3f9e8b21..."},
{"watermark_ms": 1747299600045, "sha": "a1c5b09f..."}
]
| Query param | Default | Purpose |
|---|---|---|
limit | 50 | Maximum entries returned. Capped at 500. |
before | none | Return entries older than this SHA. 7-char prefixes are accepted. |
The history endpoint lists S3 keys only; it does not read every snapshot body.
Snapshot body
body = await client.get_namespace_snapshot("products", "3f9e8b2")body, err := client.GetNamespaceSnapshot(ctx, "products", "3f9e8b2")const body = await client.getNamespaceSnapshot("products", "3f9e8b2");curl "$LAYER_GATEWAY_URL/v2/namespaces/products/snapshots/3f9e8b2" \
-H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" {
"namespace": "products",
"watermark_ms": 1747300000123,
"sha": "3f9e8b21",
"row_count": 12500,
"fields": [
{
"name": "category",
"values": [
{"v": "books", "n": 1240},
{"v": "electronics", "n": 873}
]
}
],
"fields_skipped": [
{
"name": "tags",
"reason": "exceeded_cap",
"distinct_observed": 247000,
"cap": 10000
}
]
}
fields[].values[].v is the facet listing. fields[].values[].n is the
facet count. row_count is the number of rows scanned into the snapshot;
for vector namespaces, namespace metadata
compares it with the upstream namespace row count to report indexed and
index_lag_rows. Fields present in fields[] are complete. Fields above
the 10,000 distinct-value cap are listed in fields_skipped[] instead of
being partially materialized. A skipped field is still enumerable on
demand with a values scan, which carries
a 1,000,000-value cap instead.
Activity
activity = await client.list_snapshot_activity(since=1747200000000, limit=50)activity, err := client.ListSnapshotActivity(ctx,
&hevlayer.ListSnapshotActivityParams{Since: 1747200000000, Limit: 50})const activity = await client.listSnapshotActivity({
since: 1747200000000,
limit: 50,
});curl "$LAYER_GATEWAY_URL/v2/activity/snapshots?since=1747200000000&limit=50" \
-H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" | Query param | Required | Purpose |
|---|---|---|
since | yes | Epoch-ms lower bound on ts_ms. |
limit | no | Cap 500, default 50. |
namespace | no | Exact namespace filter. |
cursor | no | Pagination cursor from next_cursor. |
Activity is snapshot lifecycle only. Search history and clickstream events have separate feeds.