Data API
data.svc serves entity CRUD, GraphQL, schema introspection, declared actions, scripted
endpoints and the migration/retention admin plane over one HTTP surface. Which entities and
endpoints exist is decided at run time by the definitions the addressed tenant resolves —
see the Data API block for the model. These pages document paths,
parameters, bodies, headers and status codes.
Base path
Section titled “Base path”The service mounts every route at a clean, unversioned path (/rest/:entity, /graphql,
/schema, /admin/data/*). The gateway matches the prefix /data/
and strips it before forwarding, so the public path is /data/ plus the service path.
public https://<host>/data/rest/invoiceservice /rest/invoiceEvery path on these pages is written in its public form. There is no version segment: paths
containing /v1/ or /v2/ are not served.
Required headers
Section titled “Required headers”Every route except the probes requires the four tenancy headers. Every route except the probes
and the anonymous mirror also requires Authorization.
| Header | Value | Missing |
|---|---|---|
Authorization | Bearer <token> issued by iam.svc | 401 |
X-Customer | customer slot of the tenant key | 400 invalid tenant |
X-Product | product slot | 400 invalid tenant |
X-Env | environment slot | 400 invalid tenant |
X-Tenant | tenant slot only — not the joined key | 400 invalid tenant |
Chassis middleware joins the four tenancy headers into customer:product:env:tenant and
resolves the tenant before any handler runs. An unresolvable tenant is rejected with a plain
400 invalid tenant that carries no error code.
curl -s https://<host>/data/rest/invoice \ -H 'Authorization: Bearer <token>' \ -H 'X-Customer: acme' -H 'X-Product: forge' -H 'X-Env: prod' -H 'X-Tenant: main'Auth modes
Section titled “Auth modes”Three gates cover the surface. All three attach the caller’s principal (user id, roles, and the impersonation actor when present) so access rules and row-level security evaluate against the real caller, and all three consume a rate-limit token.
| Mode | Routes |
|---|---|
| Bearer token | everything not listed below |
| Bearer token or API key | GET /data/rest/:entity, PATCH /data/rest, PATCH /data/bulk |
| Anonymous | /data/anon/rest/* and POST /data/anon/graphql |
The anonymous mirror drops the token, not the tenancy headers. It is still an external request: access rules and row-level security apply in full, and an anonymous principal has no user id and no roles. The mirror covers the bulk envelope, list, read-by-id, create, update, delete and search — it does not cover restore or the action alias.
Admin routes add a role gate on top: /data/admin/data/* requires role admin or
superadmin, /data/superadmin/* requires superadmin strictly. Role matching is
case-insensitive. On this deployment the destructive classes are raised to superadmin-only —
plan apply, retention runs, retention ETL handoff claim and confirm, and lock force-release.
The worksheet surface (/data/worksheets/*) takes a bearer token directly rather than through
the wrapper above. It enforces its own tenancy check — a worksheet is bound to the tenant key
that created it and returns 403 to any other — but it does not share this surface’s error
envelope or rate-limit gate. See Worksheets.
Response envelope
Section titled “Response envelope”Every REST route returns the same flat envelope. A read-by-id returns a one-element data
array, not a bare object.
{ "data": [ { "id": "01HVK...", "code": "EUR", "name": "Euro" } ], "count": 1}| Key | Type | Present |
|---|---|---|
data | array of objects | always |
count | int | always — rows in data |
total | int | when the engine reports a total or an affected-row count |
stats | object | ?stats=true — sql, duration_ms, total_count, include_sql, rows_affected |
meta | object | ?meta=true — entity, primary_key, fields[] |
states | array | ?states=true — per-row stateflow state and available events |
allowed_actions | string[] | ?allowedactions=true |
page | object | cursor pagination — has_next_page, end_cursor |
Routes outside the REST surface return their own shapes: /data/bulk returns import or export
results, /data/schema/* returns introspection documents, /data/x/* returns the script’s
return value as the entire body with no envelope, and the admin plane returns per-handler
objects.
Error body
Section titled “Error body”{ "error": "entity \"invoce\" not found", "code": "v2.entity_not_found", "details": { }}code and details are omitted when empty. Branch on code, not on the message text.
Four things break the shape:
- The admin and superadmin planes. Handler bodies under
/data/admin/data/*and/data/superadmin/*emit{"error":"<message>"}with nocodeand nodetails— on 400, 401, 403, 404 and 500 alike. Only the role gates in front of them use the coded envelope, so the same URL answers a coded 403 when the role check refuses and an uncoded 403 when the handler finds no tenant on the request. - The worksheet surface.
/data/worksheets/*nests the error:{"error":{"code","message","details","request_id"}}—erroris an object there, not a string, and the codes are unprefixed (worksheet.not_found,rate_limit_exceeded). - Auth and tenancy rejections. A missing, invalid or expired token is
401with the plain-text bodyUnauthorized. An unresolvable tenant is400with the plain-text bodyinvalid tenant. Neither is JSON. - Unrouted paths. A path no route matches returns a bare 404 with an empty body.
Status codes
Section titled “Status codes”| Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Row created (POST /data/rest/:entity) |
| 207 | Partial success — seed runs, plan apply, retention runs. The body is the source of truth |
| 400 | Malformed request: empty path segment, unparseable body, missing required parameter, unresolvable tenant |
| 401 | Token missing, invalid or expired; or a scripted endpoint resolved no principal (v2.unauthenticated) |
| 403 | No tenant in context, access denied, or a role gate refused |
| 404 | Entity, row, action, stateflow or scripted endpoint not declared |
| 405 | The verb has no matching section or declared verb. An Allow header lists what is defined |
| 422 | Validation failure, or a bulk envelope that rolled back |
| 429 | Rate limited |
| 500 | Engine or host failure |
| 501 | The feature is not wired on this deployment — file upload and download, free-text search, string-form search filters |
The worksheet surface adds statuses this table does not cover — 409, 410, 413, 502, 504 and 507. They are listed on Worksheets.
Engine failures are classified by error type first (access denied → 403, not found → 404,
validation → 422) with a substring fallback for untyped errors. Treat the code field as the
contract and the status as advisory for anything the engine raised.
Rate limiting
Section titled “Rate limiting”Every authenticated, API-key and anonymous route consumes one token from a bucket keyed by
tenant, user and profile — anonymous traffic buckets under the user segment anonymous, so it
is bounded per tenant rather than globally. Allowed requests carry the limit headers; a denial
adds Retry-After.
| Header | Value |
|---|---|
X-RateLimit-Limit | Bucket size |
X-RateLimit-Remaining | Tokens left |
X-RateLimit-Reset | Unix seconds |
Retry-After | Seconds, rounded up, minimum 1 — on 429 only |
A denial is 429 with code v2.rate_limited and details carrying profile, limit,
remaining and retry_after. The gate fails open: with no limiter installed, no request is
denied and no headers are set.
The worksheet surface runs a separate limiter. It denies with code rate_limit_exceeded, a
fixed Retry-After: 60, no details and no X-RateLimit-* headers. Retry logic keyed on
v2.rate_limited alone misses it.
Health and readiness
Section titled “Health and readiness”Both probes bypass the tenancy middleware. They take no headers, no token, and no tenant.
GET /data/ready
ready:trueReturns 200 with ready:true or 500 with ready:false, as plain text.
GET /data/health
{ "healthy": true, "dependencies": {}, "memstats": { "Alloc": 7, "HeapAlloc": 7, "HeapSys": 19, "HeapIdle": 8, "HeapInUse": 10, "TotalAlloc": 60224, "Sys": 27, "NumGC": 12653 }, "version": ""}Returns 200 when healthy, 500 when not; the body is identical either way apart from healthy
and the dependencies map. Memory values are MiB.
Route map
Section titled “Route map”| Routes | Page |
|---|---|
/data/rest/:entity, /data/rest/:entity/id/:id, restore, the /data/rest bulk envelope, the /data/anon/rest/* mirror, query parameters | REST |
POST /data/rest/:entity/search | Search |
POST /data/graphql, POST /data/anon/graphql | GraphQL |
/data/schema, /data/schema/entities*, /data/schema/graphql, the OpenAPI, JSON Schema, Zod and CUE exports | Schema and entities |
/data/actions/:name, /data/rest/:entity/action/:action, /data/x/*, /data/bulk, /data/stateflows, /data/stateflow/:name, DELETE /data/cache, GET /data/contentstore/list, /data/admin/data/*, /data/superadmin/* | Operations |
/data/worksheets/* — the 15 ephemeral SQL workbench routes | Worksheets |