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 with the Index.spec.snapshot shape.
Kubernetes operators put the shape 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.
API-only namespaces can set the same shape on the gateway:
curl -X PUT "$LAYER_GATEWAY_URL/v2/namespaces/products/snapshot-policy" \
-H "Authorization: Bearer $LAYER_GATEWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"facetFields": ["category", "brand"],
"interval": "5m",
"retention": "30d"
}'
The API policy is stored gateway-side and takes effect immediately for the
automatic writer. GET /v2/namespaces/{ns}/snapshot-policy returns the
API-managed policy for review and automation.
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-policy | GET | Read the API-managed snapshot policy. |
PUT /v2/namespaces/{ns}/snapshot-policy | PUT | Set facetFields, interval, and retention using the Index.spec.snapshot shape. |
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. |
Need a named cut for a downstream app? Use checkpoints to label the newest durable snapshot watermark without running a scan.
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...", "tags": ["pre-migration"]},
{"watermark_ms": 1747299600045, "sha": "a1c5b09f...", "tags": []}
]
| 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. tags is metadata for operator grouping and restore workflows; it is
not part of the snapshot content hash.
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.