Errors
All API errors return a consistent JSON response body. There are two error formats depending on the error source.
Validation errors
Returned with HTTP 400 when the document definition is refused before anything is rendered or billed: text that is not JSON, a key or a value the schema does not name, a required member missing, or a limit exceeded. The definition is read exactly as the published schema states it: a key in another case, a retired spelling or a definition without its root is refused, never guessed at.
{
"code": "SCHEMA",
"path": "document.content.0",
"message": "\"tabel\" is not in this schema; this object takes: source, break, p, th, td, h1, h2, h3, h4, h5, h6, title, lang, id, ol, ul, table, image, hr, columns, svg, toc, style, header, and 8 more"
}
| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code |
path | string | Where in the definition, as dotted keys and indexes (document.content.0) |
message | string | What is wrong, and what the place takes instead |
Validation error codes
| Code | Description |
|---|---|
INVALID_JSON | The request body is not JSON |
SCHEMA | A key the schema does not name, a value of the wrong kind, a required member missing, or a definition with no document or component root |
EMPTY_CONTENT | A document whose content array is empty |
UNRESOLVED_COMPONENT | A use names a component the tenant does not have |
A definition past one of the engine's resource limits (too many pages, a table or an image too large, nesting too deep) is refused with a code naming the limit, such as TOO_MANY_PAGES or NESTING_TOO_DEEP.
Examples
Typo in property name, for { "document": { "content": [{ "tabel": {} }] } }: the answer above.
Wrong type, for { "document": { "pageSetup": { "margin": true }, "content": [{ "p": "Hi" }] } }:
{
"code": "SCHEMA",
"path": "document.pageSetup.margin",
"message": "Expected a number, CSS's spacing shorthand (\"20mm\", \"10mm 20mm\") or a list of four lengths - numbers in points or unit strings (\"0.5in\", \"12.7mm\", \"48px\", \"36pt\")."
}
Empty content, for { "document": { "content": [] } }:
{
"code": "EMPTY_CONTENT",
"path": "document.content",
"message": "the content array is empty, so the document draws nothing; give it at least one node (a component may be config-only, a document may not)"
}
The root key is required
A definition is written inside document, or a component inside component. A bare { "content": [...] } is refused with SCHEMA, and the message says where to put it:
{
"code": "SCHEMA",
"path": "$",
"message": "\"content\" is not in this schema; this object takes: $schema, document, component; a definition is written inside \"document\" ({\"document\": ...}), a component inside \"component\""
}
Server errors
Returned by the global exception handler for infrastructure, authorization, and runtime errors. Uses the RFC 7807 Problem Details format.
{
"status": 503,
"title": "Service Unavailable",
"detail": "Database service is temporarily unavailable.",
"instance": "/v1/{tenantId}/documents/generate-from-payload",
"traceId": "00-abc123def456-789012-00",
"errorCode": "DATABASE_UNAVAILABLE",
"isRetryable": true,
"retryAfterSeconds": 30
}
| Field | Type | Description |
|---|---|---|
status | integer | HTTP status code |
title | string | Standard HTTP reason phrase |
detail | string | Human-readable description |
instance | string | Request path that triggered the error |
traceId | string | Unique trace ID for support requests |
errorCode | string | Machine-readable error code |
isRetryable | boolean | Whether the request can be retried |
retryAfterSeconds | integer | Seconds to wait before retrying (when retryable) |
HTTP status codes
| Status | When | Retryable |
|---|---|---|
| 400 | Invalid JSON, schema errors, empty content, bad request parameters | No |
| 401 | Missing, invalid or expired access token, or wrong client credentials | No |
| 403 | Insufficient permissions or tenant mismatch | No |
| 404 | Template or document not found | No |
| 429 | Rate limit exceeded | Yes — wait the seconds in the Retry-After header |
| 500 | PDF generation failure or unexpected server error | Maybe — retry once |
| 503 | Database, storage, or external service temporarily unavailable | Yes — check retryAfterSeconds |
Server error codes
| Code | Status | Description |
|---|---|---|
VALIDATION_ERROR | 400 | Invalid request parameters |
INVALID_OPERATION | 400 | Operation not valid in current state |
ACCESS_DENIED | 401 | Authentication failed |
TENANT_CLAIM_MISSING | 403 | Tenant information missing from token |
TENANT_MISMATCH | 403 | Accessing a resource from a different tenant |
FILE_NOT_FOUND | 404 | Requested file does not exist |
REQUEST_CANCELLED | 400 | Client cancelled the request |
DATABASE_UNAVAILABLE | 503 | Database temporarily unavailable (retryable) |
AZURE_SERVICE_ERROR | 503 | Storage service temporarily unavailable (retryable) |
EXTERNAL_SERVICE_ERROR | 503 | External dependency temporarily unavailable (retryable) |
NETWORK_ERROR | 503 | Network connectivity issue (retryable) |
SERVICE_TIMEOUT | 503 | Request timed out |
INTERNAL_ERROR | 500 | Unexpected server error |
Rate limiting
A request over a rate limit is answered 429 Too Many Requests. The Retry-After header gives the seconds to wait before
the limit resets:
HTTP/1.1 429 Too Many Requests
Retry-After: 37
{"error": "rate_limit_exceeded", "message": "Too many requests. Please try again later."}
The limits themselves are listed under Rate limits.
Best practices
- Check the HTTP status code first before parsing the response body.
- Use
codefor programmatic handling — match on error codes, not message strings. - Use
pathto locate the issue in validation errors — it points to the exact JSON property. - Retry on 429 and 503 with backoff. Use
retryAfterSecondsor theRetry-Afterheader to determine wait time. - Do not retry on 400, 401, 403, or 404 — fix the request first.
- Log
traceIdfor server errors — include it when contacting support. - Validate locally with the JSON Schema to catch errors before sending to the API.