Skip to content
Talk to our solutions team

GraphQL

data.svc serves one GraphQL endpoint per tenant, generated at run time from the tenant’s entity definitions. This page is the wire reference: paths, parameters, bodies, headers and status codes. For the generation model — type mapping, relations, input exclusions, execution behaviour — see GraphQL in the Data API block.

MethodPathAuth
POST/data/graphqlAuthorization: Bearer <token>
POST/data/anon/graphqlNone; access rules and row-level security decide what resolves

Both endpoints serve the same generated schema through the same resolvers. Only the resolved principal differs.

HeaderRequiredValue
Content-Typeyesapplication/json
Authorization/data/graphql onlyBearer <token>
X-Customer, X-Product, X-Env, X-TenantyesThe four tenancy slots the caller sends — see Data API
NameTypeDescription
datastorestringWhich datastore to address. Omit to use the schema’s default datastore.

datastore selects both the schema the document is validated against and the engine that executes it. It is the only query parameter this endpoint reads — expression and stats apply to REST routes, not here.

{
"query": "{ widgetList { id title } }",
"operationName": "",
"variables": {}
}
FieldTypeDescription
querystringThe GraphQL document.
operationNamestringWhich operation in the document to run.
variablesobjectVariable values, merged over the operation’s declared defaults.

operationName and variables are optional. When operationName is empty the first operation in the document runs — never a named default. Only that one operation executes; the rest of the document is ignored.

Names are derived from the entity name. For an entity order:

OperationFieldSignature
Get by idorderorder(id: ID!): Order
ListorderListorderList(filter: OrderFilter, limit: Int, offset: Int, orderBy: String): [Order!]!
CountorderCountorderCount(filter: OrderFilter): Int!
CreatecreateOrdercreateOrder(input: CreateOrderInput!): Order!
UpdateupdateOrderupdateOrder(id: ID!, input: UpdateOrderInput!): Order!
DeletedeleteOrderdeleteOrder(id: ID!): Boolean!

The bare entity field is a get-by-id taking a required id and returning a single object or null. It is not a list — the list field is <entity>List.

ArgumentTypeBehaviour
filter<T>FilterEquality only. Each supplied key becomes one field = value condition, ANDed.
limitIntMaximum rows returned.
offsetIntRows skipped.
orderByStringOne string, "field ASC" or "field DESC".

filter is an input object of field/value pairs, not an expression string:

query {
orderList(filter: { status: "open" }, limit: 20, offset: 40, orderBy: "createdon DESC") {
id
status
}
}

Pagination is limit + offset only. The cursor pagination available on REST list routes is not exposed in GraphQL, and <entity>Count counts rows in memory rather than pushing down a COUNT(*) — a count above 10 000 is reported as 10 000.

200 OK, Content-Type: application/json:

{
"data": {
"widgetList": [
{ "id": "w1", "title": "Alpha" },
{ "id": "w2", "title": "Beta" }
]
}
}
KeyPresent when
dataThe document parsed and an operation was selected. Omitted entirely on a parse or validation failure, and when operationName matched no operation.
errorsOne or more errors occurred. Omitted when everything resolved.

Results are keyed by the field name, or by the alias when one is given — a createWidget mutation returns data.createWidget, not data.widget. Only the fields you select come back; everything else is projected out.

StatusContent typeCondition
200application/jsonDocument ran. Includes every field-level and engine error.
400text/plainBody did not decode — invalid JSON body. An unresolvable tenant is also 400, body invalid tenant, written before the route is reached.
401text/plainToken missing, invalid or expired on /data/graphql — body Unauthorized.
403application/jsonNo tenant resolved on the request.
405text/plainGET, PUT, PATCH or DELETE — body Method Not Allowed, with Allow: OPTIONS, POST. OPTIONS gets 200 and the same header.
429application/jsonRate-limit bucket exhausted.
500application/jsonEngine or generated schema could not be built.

Only 403, 429 and 500 use the platform error envelope {"error": "...", "code": "..."}. The 400, 401 and 405 bodies are plain text and carry no code — they are written by the handler, the auth and tenancy middleware, and the router, all outside the error-envelope path.

A 429 carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and Retry-After, plus a details object with profile, limit, remaining and retry_after. Anonymous traffic is bucketed per tenant under a shared anonymous user segment.

Envelope errors:

CodeStatusMeaning
v2.no_tenant403No tenant key in the request context — the CPET headers were missing or unroutable.
v2.engine_unavailable500The tenant’s engine could not be resolved, or the generated schema failed to parse.
v2.rate_limited429Rate-limit bucket exhausted for this tenant, user and profile.

Errors inside errors[], all at HTTP 200:

MessageCause
cannot resolve field "<name>"The field validated but has no resolver. This is what introspection queries hit.
no operation "<name>" in documentoperationName matched no operation in the document.
Parse and validation messagesMalformed syntax, or a field, argument or type not in the generated schema. Nothing is dispatched and data is absent.
Engine messagesAccess denial, RLS filtering, validation failure, constraint violation or a database error, wrapped at the failing field’s path.

Introspection does not work. __schema and __type are injected into the Query type by the parser, so an introspection query passes validation and then returns 200 with data.__schema: null and cannot resolve field "__schema" in errors[]. Tools that bootstrap from introspection — GraphiQL, Apollo codegen, Relay compiler — fail against this endpoint.

Fetch the SDL instead:

Terminal window
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/data/schema/graphql
MethodPathReturns
GET/data/schema/graphqlThe generated SDL, text/plain; charset=utf-8.
GET/data/schemaSchema summary — name lists only, application/json.
GET/data/schema/entities{"entities": [{"name", "fields", "relations"}], "count"} — counts, not field detail.
GET/data/schema/entities/<entity>One entity: name, table_name, primary_key, materialization, fields[], relations[].
DELETE/data/cacheDrops the calling tenant’s cached schemas, pools and engines.

Those routes, plus the OpenAPI, JSON Schema, Zod and CUE exports, are documented in full on Schema and discovery.

The generated schema is cached per tenant and datastore and is rebuilt only when the tenant’s engine is. A definition change is invisible to this endpoint until then; DELETE /data/cache is the explicit reset, returning {"cleared": true, "tenant": "<tenant key>"}.

FeatureState
Introspection (__schema, __type)Validates, then fails to resolve.
SubscriptionsNo Subscription type is generated and no transport serves one.
Fragments and inline fragmentsParse and validate, then contribute no data and raise no error.
Directives (@skip, @include)Never evaluated.
Interfaces, unions, custom directivesNever generated.
Batched operationsOnly the one selected operation runs.
GET transport, playgroundNot served.

Aliases and variables work, at the top level and on nested fields.

POST /data/graphql?datastore=main HTTP/1.1
Content-Type: application/json
Authorization: Bearer <token>
{"query":"query { widgetList(limit: 10, orderBy: \"qty DESC\") { id title } }"}
{
"data": {
"widgetList": [
{ "id": "w1", "title": "Alpha" },
{ "id": "w2", "title": "Beta" }
]
}
}

qty is absent from the response because it was not selected, even though it drove the sort.

{
widget(id: "w1") {
id
qty
}
}
{
"data": {
"widget": { "id": "w1", "qty": 3 }
}
}

The get, update and delete fields address rows by a column named exactly id. An entity whose primary key is named anything else is not reachable through them — use <entity>List with a filter.

mutation {
createWidget(input: { title: "Created", qty: 1 }) {
id
title
}
}
{
"data": {
"createWidget": { "id": "w9", "title": "Created" }
}
}

id is not in CreateWidgetInput — auto fields are excluded and the engine’s defaults fill the primary key.

{
"query": "query List($n: Int) { widgetList(limit: $n) { id title } }",
"operationName": "List",
"variables": { "n": 10 }
}
{
"data": { "widgetList": null },
"errors": [
{ "message": "access denied", "path": ["widgetList"] }
]
}

Status is 200. The document parsed and validated; the engine refused the field.