English
Errors
Every error response of the API has the same shape, whatever the route and the cause:
json
{
"code": "workflow.not_publishable",
"message": "the draft has validation errors",
"details": {
"validation": { "ok": false, "errors": [ { "code": "trigger_required", "message": "…" } ], "warnings": [] }
}
}| Field | Meaning |
|---|---|
code | A stable identifier, part of the contract. Test this, never the message. |
message | A technical description in English, for logs and debugging. It may change without notice and is not meant for end users. |
details | Optional, specific to the error: the missing scope (required), the delay before retrying (retryAfterSeconds), the validation diagnostic (validation), the reason of a parse failure (reason)… |
The interface never shows message: it translates code into the member's language. Your program can do the same.
Codes are always domain.code
A code is a domain, a dot, and a cause in snake_case: auth.unauthenticated, workflow.not_found, api_key.scope_missing, tables.duplicate_key, request.bad_request. The domain names the part of the product; the cause names what went wrong. Codes contain only lower-case letters, digits, _ and ., and exactly one dot.
The domain lets you handle a family at once (startsWith("llm.")) and look a code up in the list of error codes. The codes specific to a route are listed under that route in the reference.
HTTP status classes
The status gives the class of the error; the code gives the precise cause.
| Status | Class | Typical codes |
|---|---|---|
400 | The request does not match its schema, or cannot be read | request.bad_request, workflow.invalid_graph, idempotency.invalid_key |
401 | No valid session or key | auth.unauthenticated |
403 | Authenticated, but refused: role, scope, policy | auth.forbidden, api_key.scope_missing, api_key.session_required, webmail.sending_disabled |
404 | Unknown resource, or a resource of another member | workflow.not_found, execution.not_found, request.not_found |
409 | A conflict with the current state | workflow.draft_conflict, workflow.not_publishable, execution.not_cancellable, idempotency.in_progress |
413 | The request body exceeds its limit (5 MB; 256 KB of JSON for a webhook trigger) | request.payload_too_large (details.reason: "FST_ERR_CTP_BODY_TOO_LARGE" for the 5 MB limit) |
422 | The request is well-formed but cannot be honoured | idempotency.key_reused |
429 | A rate limit is reached; Retry-After says when to retry | api_key.rate_limited, auth.too_many_attempts, webmail.rate_limited |
500 | An unexpected server error; nothing more is said to the client | request.internal_error |
A resource that exists but belongs to another member is a 404, never a 403: the API does not reveal what it does not let you see.
Codes common to every route
| Code | Status | When |
|---|---|---|
request.bad_request | 400 | The body or the query does not match the schema of the route, or the JSON cannot be parsed. |
request.payload_too_large | 413 | The body exceeds the limit of the route: 5 MB for the API, 256 KB of JSON for a webhook trigger. |
request.not_found | 404 | No route answers this path. |
request.internal_error | 500 | An error the server did not expect. The request id is in the server logs. |
auth.unauthenticated | 401 | No session, no key, or a key that is unknown, expired or revoked. |
auth.forbidden | 403 | The member lacks the role the route requires. |
api_key.scope_missing | 403 | None of the key's scopes covers the route; details.required names the one to add. |
api_key.session_required | 403 | The route is reserved to a member session. |
api_key.route_not_allowed | 403 | The route is not open to API keys. |
api_key.rate_limited | 429 | More than 600 requests in the last minute with this key. |
idempotency.invalid_key | 400 | Idempotency-Key is empty or longer than 200 characters. |
idempotency.in_progress | 409 | The original request with this key is still running. |
idempotency.key_reused | 422 | The same key was used with a different request. |
Only the first two are listed under each route in the OpenAPI document (as the 400, 401, 403 and 429 responses); the others are implied for every authenticated route.
Handling errors in a client
- Branch on
code, and on the status only as a fallback for codes you do not know yet. - On
429, honourRetry-After. On409 idempotency.in_progress, replay with the sameIdempotency-Keyafter a second. - On
403 api_key.scope_missing, the fix is a new key withdetails.required, not a retry. - On
409 workflow.not_publishable, readdetails.validation.errors: each entry carries the node (nodeId) and the reason (code), the same diagnostic the editor shows. - Log
messageanddetails; show your users a text of your own, keyed oncode.