Skip to main content

Errors

All errors share a single envelope:

{
"error": {
"code": "validation_failed",
"message": "Field 'email' is required",
"details": {
"field": "email"
}
}
}
  • code is a machine-readable identifier. Stable across releases.
  • message is human-readable English. Localised strings are not provided — translate at the consumer.
  • details is optional and shape-specific to the error. For validation errors it includes the offending field; for rate-limit errors it includes a numeric retry_after.

Standard codes​

HTTPcodeWhen
400bad_requestMalformed JSON, unsupported Content-Type, etc.
400validation_failedBody or query failed schema validation.
401mtls_requiredClient certificate missing/invalid.
401invalid_tokenJWT malformed or signature invalid.
401invalid_audienceaud does not match the environment.
401token_expiredexp claim in the past.
401token_revokedjti listed in RevokedTokens.
403insufficient_scopeToken lacks the required scope.
403insufficient_permissionThe person behind the token lacks the permission the endpoint needs; message is the permission slug.
403forbiddenOrigin shared-secret missing or wrong, or the token resolves to no tenant — message is unknown_organization, no_membership or unknown_workspace. See Authentication.
400workspace_requiredThe organization has several workspaces and X-Workspace-Id was not sent.
404not_foundResource does not exist (or belongs to another workspace).
409conflictState conflict (e.g. idempotency key reused with a different body, unique constraint violation).
422unprocessable_entityBusiness rule violation (e.g. cannot deactivate a webhook with active retries).
429rate_limitedPer-account limit exceeded. Retry-After header included.
500internal_errorUnhandled error. Correlation id returned in details.request_id.
503service_unavailableUpstream dependency (S3, DB, AI Gateway) failing.

Reading details​

details is intentionally loose. Common patterns:

  • Validation errors — { "field": "subscribed_events.0", "issue": "must be a known SystemEvent slug" }.
  • Rate limiting — { "retry_after": 17, "limit": 600, "remaining": 0 }.
  • Idempotency conflict — { "key": "...", "original_status": 201, "original_response_id": "..." }.
  • Not found across workspace boundary — never leaks the existence of the resource; the response is indistinguishable from a truly missing id.

Correlation​

Every response — successful or not — carries a X-Request-Id header. Include it when reporting issues so we can pull the trace from OpenTelemetry.

Retry guidance​

Error classRetry?
400, 401, 403, 404, 409, 422No — fix the request.
429Yes, after Retry-After seconds.
500, 503Yes, with exponential backoff (start at 1s, cap at 30s, max 5 attempts).

For idempotent operations (GET, PATCH, DELETE) retrying is always safe. For POST operations always include an X-Idempotency-Key so retries don't double-create.