Skip to content

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": [] }
  }
}
FieldMeaning
codeA stable identifier, part of the contract. Test this, never the message.
messageA technical description in English, for logs and debugging. It may change without notice and is not meant for end users.
detailsOptional, 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.

StatusClassTypical codes
400The request does not match its schema, or cannot be readrequest.bad_request, workflow.invalid_graph, idempotency.invalid_key
401No valid session or keyauth.unauthenticated
403Authenticated, but refused: role, scope, policyauth.forbidden, api_key.scope_missing, api_key.session_required, webmail.sending_disabled
404Unknown resource, or a resource of another memberworkflow.not_found, execution.not_found, request.not_found
409A conflict with the current stateworkflow.draft_conflict, workflow.not_publishable, execution.not_cancellable, idempotency.in_progress
413The 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)
422The request is well-formed but cannot be honouredidempotency.key_reused
429A rate limit is reached; Retry-After says when to retryapi_key.rate_limited, auth.too_many_attempts, webmail.rate_limited
500An unexpected server error; nothing more is said to the clientrequest.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 ​

CodeStatusWhen
request.bad_request400The body or the query does not match the schema of the route, or the JSON cannot be parsed.
request.payload_too_large413The body exceeds the limit of the route: 5 MB for the API, 256 KB of JSON for a webhook trigger.
request.not_found404No route answers this path.
request.internal_error500An error the server did not expect. The request id is in the server logs.
auth.unauthenticated401No session, no key, or a key that is unknown, expired or revoked.
auth.forbidden403The member lacks the role the route requires.
api_key.scope_missing403None of the key's scopes covers the route; details.required names the one to add.
api_key.session_required403The route is reserved to a member session.
api_key.route_not_allowed403The route is not open to API keys.
api_key.rate_limited429More than 600 requests in the last minute with this key.
idempotency.invalid_key400Idempotency-Key is empty or longer than 200 characters.
idempotency.in_progress409The original request with this key is still running.
idempotency.key_reused422The 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, honour Retry-After. On 409 idempotency.in_progress, replay with the same Idempotency-Key after a second.
  • On 403 api_key.scope_missing, the fix is a new key with details.required, not a retry.
  • On 409 workflow.not_publishable, read details.validation.errors: each entry carries the node (nodeId) and the reason (code), the same diagnostic the editor shows.
  • Log message and details; show your users a text of your own, keyed on code.