Skip to content
Talk to our solutions team

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.

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/invoice
service /rest/invoice

Every path on these pages is written in its public form. There is no version segment: paths containing /v1/ or /v2/ are not served.

Every route except the probes requires the four tenancy headers. Every route except the probes and the anonymous mirror also requires Authorization.

HeaderValueMissing
AuthorizationBearer <token> issued by iam.svc401
X-Customercustomer slot of the tenant key400 invalid tenant
X-Productproduct slot400 invalid tenant
X-Envenvironment slot400 invalid tenant
X-Tenanttenant slot only — not the joined key400 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.

Terminal window
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'

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.

ModeRoutes
Bearer tokeneverything not listed below
Bearer token or API keyGET /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.

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
}
KeyTypePresent
dataarray of objectsalways
countintalways — rows in data
totalintwhen the engine reports a total or an affected-row count
statsobject?stats=truesql, duration_ms, total_count, include_sql, rows_affected
metaobject?meta=trueentity, primary_key, fields[]
statesarray?states=true — per-row stateflow state and available events
allowed_actionsstring[]?allowedactions=true
pageobjectcursor 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": "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 no code and no details — 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"}}error is 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 401 with the plain-text body Unauthorized. An unresolvable tenant is 400 with the plain-text body invalid tenant. Neither is JSON.
  • Unrouted paths. A path no route matches returns a bare 404 with an empty body.
CodeMeaning
200Success
201Row created (POST /data/rest/:entity)
207Partial success — seed runs, plan apply, retention runs. The body is the source of truth
400Malformed request: empty path segment, unparseable body, missing required parameter, unresolvable tenant
401Token missing, invalid or expired; or a scripted endpoint resolved no principal (v2.unauthenticated)
403No tenant in context, access denied, or a role gate refused
404Entity, row, action, stateflow or scripted endpoint not declared
405The verb has no matching section or declared verb. An Allow header lists what is defined
422Validation failure, or a bulk envelope that rolled back
429Rate limited
500Engine or host failure
501The 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.

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.

HeaderValue
X-RateLimit-LimitBucket size
X-RateLimit-RemainingTokens left
X-RateLimit-ResetUnix seconds
Retry-AfterSeconds, 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.

Both probes bypass the tenancy middleware. They take no headers, no token, and no tenant.

GET /data/ready

ready:true

Returns 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.

RoutesPage
/data/rest/:entity, /data/rest/:entity/id/:id, restore, the /data/rest bulk envelope, the /data/anon/rest/* mirror, query parametersREST
POST /data/rest/:entity/searchSearch
POST /data/graphql, POST /data/anon/graphqlGraphQL
/data/schema, /data/schema/entities*, /data/schema/graphql, the OpenAPI, JSON Schema, Zod and CUE exportsSchema 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 routesWorksheets