Skip to content
Talk to our solutions team

Schema and discovery

These are the routes a client calls to learn what entities exist and what shape they have. Paths are shown as an external client sees them, with the /data/ prefix the gateway strips before forwarding; the service itself serves the discovery and export routes at /schema/... and the two catalogue routes at /admin/data/....

What each export format contains, which variants a field appears in, and the field-type mappings are on Schema export and discovery. This page is the wire contract: paths, parameters, bodies, and status codes.

MethodPathReturnsContent type
GET/data/schemaSchema summary — name lists onlyapplication/json
GET/data/schema/entitiesEntity inventory with countsapplication/json
GET/data/schema/entities/{entity}One entity’s fields and relationsapplication/json
GET/data/schema/graphqlGenerated GraphQL SDLtext/plain; charset=utf-8
GET/data/schema/openapi.jsonOpenAPI 3.1 documentapplication/json; charset=utf-8
GET/data/schema/jsonschema.jsonJSON Schema 2020-12 bundleapplication/schema+json; charset=utf-8
GET/data/schema/jsonschema/{entity}JSON Schema for one entityapplication/schema+json; charset=utf-8
GET/data/schema/zod.tsZod TypeScript bundleapplication/typescript; charset=utf-8
GET/data/schema/zod/{entity}Zod for one entityapplication/typescript; charset=utf-8
GET/data/schema/cueCUE bundletext/x-cue; charset=utf-8
GET/data/schema/cue/{entity}CUE for one entitytext/x-cue; charset=utf-8
GET/data/admin/data/references/{entity}Foreign keys pointing at an entityapplication/json
GET/data/admin/data/hashesCompiled-query catalogueapplication/json

Every route on this page is GET and takes the four tenant headers plus a bearer token:

GET /data/schema/entities/order HTTP/1.1
Host: api.example.com
X-Customer: acme
X-Product: erp
X-Env: prod
X-Tenant: main
Authorization: Bearer <token>
ParameterInRequiredMeaning
datastorequerynoWhich datastore to describe. Omit for the schema’s default.
entitypathon per-entity routesExact entity name. Each export route trims its own extension, so order and order.json are the same request on the JSON Schema route.

There is no anonymous mirror for any of these routes. /data/anon/rest/* and /data/anon/graphql serve unauthenticated callers, but schema discovery and the exports always require a token — a published anonymous integration has to be handed its schema out of band.

GET /data/schema returns names only, never definitions. Every list is a sorted array of strings.

{
"name": "erp",
"version": "1.4.0",
"default_datastore": "main",
"datastores": ["main", "reporting"],
"entities": ["order", "order_item"],
"relations": [],
"types": ["credit_score"],
"enums": ["OrderStatus"],
"traits": ["auditable"],
"embeddables": ["address"],
"stateflows": ["order_lifecycle"],
"views": [],
"functions": []
}

GET /data/schema/entities lists every entity with counts, sorted by name. fields and relations are integers, not arrays.

{
"entities": [
{ "name": "order", "fields": 12, "relations": 2 },
{ "name": "order_item", "fields": 7, "relations": 1 }
],
"count": 2
}

GET /data/schema/entities/{entity} is the shape call. Both fields and relations come back in declaration order.

{
"name": "order",
"table_name": "orders",
"primary_key": "id",
"materialization": 0,
"fields": [
{
"name": "id",
"type": "",
"required": true,
"nullable": false,
"unique": true,
"primary_key": true,
"computed": false,
"default": "ulid()"
},
{
"name": "risk_band",
"type": "credit_score",
"required": false,
"nullable": true,
"unique": false,
"primary_key": false,
"computed": false,
"default": null
}
],
"relations": [
{
"name": "items",
"type": 2,
"to_entity": "order_item",
"from_field": "id",
"to_field": "order_id"
}
]
}
Top-level keyTypeMeaning
namestringEntity name as declared
table_namestringPhysical table, which may differ from name
primary_keystringSingle-column primary key
materializationintStorage ordinal — 0 table, 1 view, 2 remote, 3 contract, 4 virtual
fieldsarrayColumns, in declaration order
relationsarrayRelations, in declaration order
Field keyTypeMeaning
namestringColumn name as declared
typestringDeclared type spelling, or empty — see the caution below
requiredboolDeclared required
nullableboolDeclared nullable
uniqueboolDeclared unique
primary_keyboolTrue when the name matches the entity’s primary_key
computedboolTrue for computed fields
defaultanyDeclared default; null when unset
Relation keyTypeMeaning
namestringRelation name, and the include key used to eager-load it
typeintCardinality ordinal — 0 unspecified, 1 one-to-one, 2 one-to-many, 3 many-to-one, 4 many-to-many
to_entitystringTarget entity
from_fieldstringColumn on this entity
to_fieldstringColumn on the target entity

Fields carrying a pii, phi, pci, gdpr or noexport compliance marker are still listed here. That gate applies to the export formats only, not to this route or the SDL.

GET /data/schema/graphql returns the generated SDL as plain text, not JSON.

Terminal window
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/data/schema/graphql

Four of the eight renderers are served over HTTP. Each has a bundle route and, except OpenAPI, a per-entity route.

FormatBundlePer-entity
OpenAPI 3.1GET /data/schema/openapi.jsonnone
JSON Schema 2020-12GET /data/schema/jsonschema.jsonGET /data/schema/jsonschema/{entity}
Zod (TypeScript)GET /data/schema/zod.tsGET /data/schema/zod/{entity}
CUEGET /data/schema/cueGET /data/schema/cue/{entity}
Terminal window
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/data/schema/jsonschema/order.json

The per-entity JSON Schema route emits that entity’s three variants plus only the enums its fields reference. The CUE bundle path has no file extension.

The Protocol Buffers, Avro, OData CSDL and AsyncAPI renderers have no route — there is no way to fetch a .proto, an .avsc, an OData $metadata document or an AsyncAPI document over HTTP.

GET /data/admin/data/references/{entity} lists every foreign key pointing at the named entity — the “what breaks if I drop this” view. Rows are sorted by from_entity, then from_field.

{
"tenant": "acme:erp:prod:main",
"datastore": "main",
"entity": "order",
"count": 1,
"references": [
{
"from_entity": "order_item",
"from_field": "order_id",
"to_field": "id",
"on_delete": "cascade",
"on_update": "noaction"
}
]
}

on_delete and on_update render as cascade, setnull, restrict or noaction. Both keys are always present: noaction is the zero value, so a foreign key that declares neither behaviour reports noaction rather than omitting the key. datastore echoes the ?datastore= parameter verbatim and is "" when you omit it. Outgoing references are not returned; read them from the target entity’s own detail response.

GET /data/admin/data/hashes is the replacement for the retired /data/hashes/list.

{
"tenant": "acme:erp:prod:main",
"datastore": "main",
"count": 0,
"hashes": []
}

The two /data/admin/data/* routes above sit behind the admin policy’s Read gate, which admits the admin and superadmin roles (role matching is case-insensitive). Both have superadmin mirrors that target a sibling tenant by name — /data/superadmin/tenant/{tenant}/references/{entity} and /data/superadmin/tenant/{tenant}/hashes — where {tenant} is the bare tenant segment, not a full four-part key.

For /data/schema/*:

StatusCodeCause
200Success
400v2.missing_entityThe {entity} path segment was empty
403v2.no_tenantNo tenant key on the request — tenant headers missing, or the service was called directly
404v2.entity_not_foundThe entity is not in the tenant’s resolved schema; check the spelling and ?datastore=
429v2.rate_limitedThe caller’s rate-limit bucket is empty
500v2.engine_unavailableThe tenant’s engine could not be resolved or built
500v2.export_render_failedA renderer failed while producing the document

Every route on this page is wrapped by the authenticated gate, which spends one rate-limit token per request. A 429 carries Retry-After alongside the X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers the successful responses also set.

For the two admin routes, a missing role returns 403 with the coded envelope; the other conditions return 400, 404 and 500 with the bare {"error": "..."} body described above.

These routes render from the tenant’s cached schema. Loading new definitions does not change what they return until the cached engine is dropped with DELETE /data/cache, documented on Sync, transfer, and schema.

Retired routeReplacement
GET /data/entities/listGET /data/schema/entities
GET /data/entities/list/{entity}GET /data/schema/entities/{entity}
GET /data/entities/references/{entity}GET /data/admin/data/references/{entity}
GET /data/hashes/listGET /data/admin/data/hashes
POST /data/entities/index/{entity}none
POST /data/entities/validateNo direct replacement. A dry-run plan generation covers the same check — see Sync, transfer, and schema.

The tag and store filters the retired list routes accepted do not exist on any replacement. ?datastore= is the only selector.