Error codes
data.svc returns 64 distinct code values on its main API surface, 21 more on the worksheet
surface, and no code at all on the admin plane. This page is the complete list. Branch on code,
not on the status: several codes carry a status that is chosen at response time from the
underlying error.
Envelopes
Section titled “Envelopes”Three shapes, and one route class that is not JSON at all.
| Surface | Body |
|---|---|
Main API (/data/rest/*, /data/anon/rest/*, /data/schema/*, /data/actions/*, /data/x/*, /data/stateflow*, /data/bulk, /data/cache, /data/contentstore/list) | {"error":"<message>","code":"v2.<slug>","details":{...}} — code and details omitted when empty |
Worksheets (/data/worksheets/*) | {"error":{"code":"<area>.<slug>","message":"…","details":{…},"request_id":"…"}} — error is an object |
Admin plane (/data/admin/data/*, /data/superadmin/*) | {"error":"<message>"} — no code, no details |
| Authentication rejection (any protected route) | Plain text Unauthorized, Content-Type: text/plain; charset=utf-8 |
details is populated on eight codes only:
| Code | details |
|---|---|
v2.bulk_failed | operations — the partial per-operation result array |
v2.rate_limited | profile, limit, remaining, retry_after (seconds) |
v2.action_method_not_allowed | allowed — array of verbs |
v2.method_not_allowed | allowed — a single verb string, not an array |
v2.file_storage_unconfigured | entity |
v2.file_downloader_unconfigured | entity, id |
v2.search_text_not_supported | entity, audit |
v2.search_dsl_not_supported | entity, got — the Go type of the rejected filter — and audit |
How the status is chosen
Section titled “How the status is chosen”Six codes — v2.query_failed, v2.create_failed, v2.update_failed, v2.delete_failed,
v2.restore_failed, v2.search_query_failed — name the operation that failed, not the reason.
The status carries the reason, and is derived from the engine error:
| Engine error | Status |
|---|---|
| Access rule or row-level rule denied the operation | 403 |
| Row or entity not found | 404 |
| Field, entity or business-constraint validation failed | 422 |
| Anything else | 500 |
Classification is by error type first. A substring fallback covers the paths that still return
untyped errors: access denied / not authorised / not authorized / forbidden → 403,
not found → 404, validation / invalid / is required → 422, everything else → 500.
Tenancy and routing
Section titled “Tenancy and routing”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
v2.no_tenant | 403 | No tenant key was resolved into the request context | Missing or unroutable tenant headers, or the tenant is not configured on this deployment |
v2.missing_tenant | 400 | The :tenant path parameter is required on this superadmin route | Superadmin URL built without the tenant segment |
v2.invalid_tenant | 400 | The :tenant value must be the bare tenant name, not a full four-part key | Customer, product and environment come from the request, not the path |
v2.bad_path | 400 | A required path segment is empty — entity, id, action name, or entity plus action | Request against a route shape the handler does not recognise |
Request body and parameters
Section titled “Request body and parameters”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
v2.bad_json | 400 | The body is not valid JSON, or the bulk envelope failed to parse | Malformed payload |
v2.body_read | 400 | The body could not be read off the wire | Truncated request, client disconnect, oversized body |
v2.bad_params | 400 | Action parameters failed to bind | Unreadable body, or invalid JSON in an action call |
v2.missing_id | 400 | An update needs a primary key and none was supplied | PUT/PATCH /data/rest/{entity} with no key in the body and no /id/{id} in the path |
v2.id_mismatch | 422 | The primary key in the body contradicts the one in the path | The key is immutable; send it consistently or omit it |
v2.bad_expression | 400 | The ?expression= program failed to compile | Syntax error in the response-shaping expression |
v2.expression_runtime | 500 | The expression compiled but failed at evaluation, or exceeded its time ceiling | Reference to a field absent from the result, or an illegal operation |
Expression semantics are on request flags.
Definition and row lookup
Section titled “Definition and row lookup”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
v2.entity_not_found | 404 | The entity is not in the tenant’s composed schema | Typo, or the definition was never loaded for this tenant and product |
v2.row_not_found | 404 | No row with that id | Wrong id, or the row is soft-deleted and therefore invisible to reads |
v2.missing_entity | 400 | No entity was supplied, as a path segment or as ?entity= | Bulk import/export without ?entity=, or a schema route without the entity segment |
v2.stateflow_not_found | 404 | No stateflow registered under that name | Typo, or the flow is not in the composed schema |
v2.missing_name | 400 | The stateflow name path segment is empty | Detail route called with no name |
v2.row_not_found after a successful DELETE is expected: delete is soft by default. See
traits.
Reads, writes and search
Section titled “Reads, writes and search”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
v2.query_failed | 403 / 404 / 422 / 500 | A read failed in the engine | Access denial, missing row, invalid filter, database error |
v2.create_failed | 403 / 404 / 422 / 500 | A create failed | Validation failure, unique or foreign-key violation, access denial |
v2.update_failed | 403 / 404 / 422 / 500 | An update failed | final / readonly / writeonce violation, validation failure, rejected state transition, access denial |
v2.delete_failed | 403 / 404 / 422 / 500 | A delete failed | Foreign-key restriction from a child row, access denial |
v2.restore_failed | 403 / 404 / 422 / 500 | A soft-delete restore failed | Most often restore: entity %q has no soft-delete columns — the entity does not carry the trait |
v2.search_query_failed | 403 / 404 / 422 / 500 | A structured search failed | Unfilterable field, bad operator, access denial |
v2.search_body_parse_failed | 400 | The search body is not decodable JSON | Malformed body on POST /data/rest/{entity}/search |
v2.search_text_not_supported | 501 | Free-text search is not implemented | Sending query instead of a filter object; no search client is wired |
v2.search_dsl_not_supported | 501 | A filter supplied as a string is not interpreted | Send filter as a JSON object |
Operators and filter shapes are on query DSL; the per-rule failure messages are on validations and stateflows.
Bulk envelope, import and export
Section titled “Bulk envelope, import and export”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
v2.empty_envelope | 400 | The operations array is empty or absent | Envelope posted with no operations |
v2.bulk_failed | 422 | The bulk transaction rolled back | Any operation failed; details.operations carries the partial results |
v2.op_failed | — | Per-operation code inside the response body, not a status | Set as error_code on the failing entry, alongside an error string |
v2.bad_format | 400 | Unsupported import/export format | ?format= is not csv, json or jsonl (ndjson is accepted as an alias for jsonl) |
v2.import_failed | 500 | A bulk import failed mid-stream | Malformed source rows, type coercion failure, or a database error |
The whole envelope runs in one transaction: one failure rolls everything back. The 422 body still carries the per-operation array for diagnostics — treat it as a report, not as a partial success.
Actions and query hashes
Section titled “Actions and query hashes”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
v2.action_not_found | 404 | No action or query hash registered under that name | Typo, or the definition is not part of the tenant’s composed schema |
v2.action_method_not_allowed | 405 | The action declares no section for this verb | An Allow header lists the accepted verbs and details.allowed repeats them |
v2.action_failed | 500 | The action executed and errored | SQL error, script error, downstream call failure |
v2.registry_unavailable | 500 | The query-hash registry could not be resolved | Registry provider not wired, or tenant configuration incomplete |
Parameter binding refusals surface as v2.action_failed with messages naming the parameter:
unknown parameter, missing required parameter, value not in enum.
Scripted endpoints
Section titled “Scripted endpoints”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
v2.endpoint_not_found | 404 | No endpoint at that path — including one that exists but is disabled | A disabled endpoint deliberately answers 404, not 403, so its existence is not disclosed |
v2.endpoint_path_missing | 404 | The dispatch catch-all matched with an empty path | Request to the script root with no endpoint segment |
v2.method_not_allowed | 405 | The endpoint declares one verb and the request used another | Allow header and details.allowed name the expected verb |
v2.unauthenticated | 401 | The endpoint requires a principal and none was resolved | Missing or expired token. This 401 is JSON — unlike the middleware’s |
v2.forbidden | 403 | The endpoint’s auth gate rejected the principal | Principal lacks what the endpoint requires; also returned for an unknown auth mode in the declaration |
v2.bad_body | 400 | The body is unreadable, not JSON, or over 1 MiB | Body shape does not match the endpoint’s declaration |
v2.script_error | 500 | The script failed to compile, or threw at runtime | Any uncaught error in the endpoint body; also a file: script whose bytes were never inlined |
v2.script_engine_missing | 500 | The endpoint registry was built without a script engine | Deployment wiring bug. An unsupported language: does not land here — it is refused at registration, so the endpoint answers v2.endpoint_not_found |
See scripted endpoints.
| Code | Status | Meaning | Likely cause |
|---|---|---|---|
v2.file_storage_unconfigured | 501 | No file-storage backend is registered | Any multipart create |
v2.file_downloader_unconfigured | 501 | No downloader is registered | Any ?download=true read |
v2.multipart_parse | 400 | The multipart body could not be parsed | Bad boundary or truncated upload |
v2.multipart_empty | 400 | Multipart parsed but produced no form | Upload with no file and no fields |
v2.multipart_bad_payload | 400 | A file part targets an undeclared field, targets a non-file field, carries more than one file for a field, or could not be read | Wrong field names, or an array upload against a single-file field |
v2.download_needs_id | 400 | Download requires the /id/{id} form | ?download=true used on a list path |
v2.missing_field | 400 | Download requires ?field= | Field name omitted |
v2.field_not_found | 404 | The field does not exist on the entity | Typo |
v2.field_not_file | 400 | The field is not file-typed | Download requested against a scalar |
v2.field_empty | 404 | The file field exists but holds no reference on this row | Nothing was ever uploaded |
v2.field_unexpected_shape | 500 | The stored reference is present but not a string | Data written outside the API, or a storage-format change |
v2.download_failed | 500 | The byte stream could not be fetched | Storage outage, expired credentials, deleted object |
v2.contentstore_list_failed | 500 | The content-store catalogue could not be read | Backing store unavailable |
Access, roles and rate limiting
Section titled “Access, roles and rate limiting”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
v2.requires_admin | 403 | The route requires admin or superadmin | Role matching is case-insensitive, and superadmin also satisfies admin |
v2.requires_superadmin | 403 | The route requires superadmin strictly | Plan apply, retention and lock force-release are superadmin-gated even for tenant admins |
v2.rate_limited | 429 | The rate-limit bucket for this tenant, user and profile is exhausted | Anonymous traffic is rate-limited too |
A v2.rate_limited response carries Retry-After plus X-RateLimit-Limit,
X-RateLimit-Remaining and X-RateLimit-Reset.
One further code, v2.authz_denied (403), exists in the catalogue but data.svc never returns it:
it is raised only on alias-mounted CRUD routes, and data.svc mounts none. Where it does apply it
fires only when an authorizer is registered — with none registered the check fails open and the
request proceeds. It is not a substitute for
access rules, which deny by default.
Entity-level and row-level denials never carry these codes — they arrive as the CRUD code with a 403 status. See row-level security and field protection.
Schema export and cache
Section titled “Schema export and cache”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
v2.export_render_failed | 500 | An export failed to render | A schema construct the exporter cannot represent, or a template failure |
v2.cache_clear_failed | 500 | Clearing the caller’s tenant schema cache failed | Cache backend unavailable |
v2.invalidate_failed | 500 | Superadmin cache invalidation for a target tenant failed | Cache backend unavailable, or the tenant does not resolve |
v2.list_tenants_failed | 500 | The superadmin tenant listing failed | Configuration client unavailable |
See schema export and caching.
Engine and service
Section titled “Engine and service”| Code | Status | Meaning | Likely cause |
|---|---|---|---|
v2.engine_unavailable | 500 | The per-tenant engine bundle could not be resolved | Tenant configuration not loaded yet, definitions failed to compile, no datastore declared, or the database is unreachable |
v2.engine_unexpected_type | 500 | The resolved engine does not implement the interface this surface needs | Deployment wiring bug |
v2.engine_unavailable is the most common boot-order failure. The wrapped message names the
failing step. See schema sources and
datastores.
GraphQL
Section titled “GraphQL”GraphQL does not use the JSON error envelope. Field-level failures return 200 with the error
in the errors array and the field set to null — including access denials, validation failures
and constraint violations. Two transport-level rejections are plain text, not JSON:
| Condition | Status | Body |
|---|---|---|
Any verb other than POST | 405 | method not allowed |
| Unparseable request body | 400 | invalid JSON body |
v2.no_tenant (403) and v2.engine_unavailable (500) are still returned in the JSON envelope,
because they are raised before the handler resolves. Introspection is not served: __schema and
__type validate but have no resolver, so they come back as cannot resolve field. See
GraphQL.
Worksheets
Section titled “Worksheets”The worksheet surface has its own catalogue and its own envelope. Statuses come from a fixed table, not from message classification.
| Code | Status | Meaning |
|---|---|---|
worksheet.not_found | 404 | No worksheet with that id, or it has been evicted |
worksheet.expired | 410 | The TTL passed; the worksheet is inside the grace window |
worksheet.cpet_violation | 403 | The worksheet belongs to a different tenant |
worksheet.invalid_ttl | 400 | Requested TTL is outside the allowed range |
worksheet.memory_exceeded | 507 | The worksheet exceeded its memory budget |
file.not_found | 404 | No staged file with that id |
file.uri_unreachable | 502 | A file referenced by URI could not be fetched |
file.uri_access_denied | 403 | The remote store refused the fetch |
file.format_invalid | 400 | Format unrecognised or unparseable; also when the multipart form has no file part |
file.size_exceeded | 413 | Over the file-size, file-count, row or column limit |
file.encoding_error | 400 | The character encoding could not be decoded |
file.sheet_selection_required | 400 | A multi-sheet workbook needs an explicit sheet |
table.name_conflict | 409 | A table with that name already exists |
table.not_found | 404 | No such table in the worksheet |
table.union_incompatible | 400 | Column sets or types do not line up |
column.not_found | 404 | No such column on the table |
query.syntax_error | 400 | The SQL failed to parse |
query.timeout | 504 | The query exceeded its time budget |
query.execution_error | 400 | The query parsed but failed at execution |
invalid_request | 400 | Invalid JSON body on a worksheet call |
rate_limit_exceeded | 429 | Worksheet rate limit; sends Retry-After: 60 |
Limits and the values behind each code are on worksheet limits.
Admin and superadmin plane
Section titled “Admin and superadmin plane”Every handler under /data/admin/data/* and /data/superadmin/* returns {"error":"<message>"}
with no code and no details. Only the auth gates in front of them use the coded envelope, so
v2.requires_admin, v2.requires_superadmin, v2.missing_tenant and v2.invalid_tenant do
appear on these routes — but nothing the handler itself raises does.
| Status | Message shapes |
|---|---|
| 400 | plan_id query parameter required, plan id missing in path, missing :resource segment, claimer_id is required, confirmations: empty, entity path segment missing, invalid confirm_max_risk %q (use low / medium / high), body parse failures |
| 401 | Always missing tenant identity |
| 403 | tenant key missing from request context |
| 404 | plan %q not found, entity <name> not found in schema |
| 500 | Wrapped operational errors; engine resolution failures are prefixed engine_unavailable: inside the message string, not carried as a code |
Plan apply is the exception to the table: it answers 207 whenever the result is anything other
than fully applied, and carries a partial result object rather than an error body —
{plan_id, status, steps_applied, steps_failed, steps_skipped, failed_step?, halted_step?, error?}. It falls back to 500 with the plain {"error":"<message>"} shape only when the apply
produced no result at all. See migrations.
What is not a wire code
Section titled “What is not a wire code”Three families of identifier look like error codes and are not.
Per-rule validation codes. REQUIRED, READONLY, FINAL, WRITE_ONCE, MIN, MAX,
MIN_LENGTH, MAX_LENGTH, LENGTH, PATTERN, the format-checker codes (EMAIL, URL,
URL_PROTOCOL, UUID, ULID, CUID, NANOID, SEMVER, PHONE, IP, HOSTNAME,
MACADDRESS, PORT, JSON, DATAURI, CREDITCARD, LATITUDE, LONGITUDE, MIMETYPE,
FILESIZE_MIN, FILESIZE_MAX, CONTAINS, NOTCONTAINS, STARTSWITH, ENDSWITH, NOTIN,
DIVISIBLEBY, BEFORE, AFTER, PAST, FUTURE, PATTERN_UNKNOWN, VALIDATION_UNKNOWN), and
the defaults ENTITY_VALIDATE, ENTITY_VALIDATE_ERR, BUSINESS_CONSTRAINT,
BUSINESS_CONSTRAINT_ERR, POINTCUT_ERR, POINTCUT_ABORT are set on the internal validation
error and never serialised. The response carries the CRUD code and the rule’s message only.
Schema-compile codes. ENTITY_NOT_FOUND, FIELD_NOT_FOUND, TYPE_MISMATCH,
INVALID_ENUM_VALUE, RELATION_NOT_FOUND, FIELD_NOT_QUERYABLE, UNSUPPORTED_OPERATION,
DUPLICATE_ENTITY, DUPLICATE_FIELD, SHADOW_PRODUCT_FIELD and INVALID_TRANSITION are declared
constants with no emitter — nothing in the tree raises one. The schema front-end the deployable
service uses reports load failures as plain wrapped errors naming the failing step, not as coded
ones. A schema that fails to load never serves traffic, so a client sees nothing from this family
either way. Do not branch on these names.
The legacy data-* catalogue. A large set of codes spelled data-<something> exists in the
tree and is dead: no handler emits any of them. Do not build client branching on that list.