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, a bulk envelope that rolled back, or an update whose body primary key contradicts the one in the path |
| 429 | Rate limited |
| 500 | Engine or host failure |
The status comes from the type of the engine error, not from the text of its message. That matters where a message is authored by you: a custom validation message, a rule’s refusal text, or a stateflow guard’s explanation cannot move a response between 422 and 500 by the words it happens to contain.
An update addressed by id targets the row through the path. The primary key is not written into the update, so an immutable-id declaration is not violated by the act of updating a row by id; and a body that names a different id is refused with 422 rather than one of the two being chosen for you.
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. With no limiter configured, 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": {}, "version": ""}The response also carries a memstats object. That is runtime diagnostics, not a contract — see
API conventions. version is whatever the service
set at startup, and most set nothing.
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 |