Skip to main content

Errors

Every error response of the HTTP API, on every family (tenant API, client API, inbound webhooks), from controllers, authentication and rate-limit plugs, and the exception handlers (unknown route, malformed request, crash), has the same body (ADR-0050):

{
"error": {
"code": "conversation_closed",
"message": "Conversation is closed",
"details": null,
"request_id": "GBHDfGZ0bW5Sl2YAAAEB"
}
}
KeyTypeMeaning
codestringStable, machine-readable key from the list below. Branch on this.
messagestringHuman-readable explanation. It may change between releases: show it, log it, never parse it.
detailsobject or nullCode-specific data, see details.
request_idstring or nullThe request's x-request-id header (Plug.RequestId), also attached to the server log lines of that request. Quote it when reporting a problem.

All four keys are always present. The body is described by the ErrorResponse schema of the OpenAPI document, and every operation lists the error statuses it can return.

The implementation is ConvergerWeb.APIError (the code catalog), ConvergerWeb.FallbackController (controller errors) and ConvergerWeb.ErrorJSON (exceptions: the code is derived from the status and the message is the code's generic message, so no exception text leaks).

Error codes​

The HTTP status is the code's usual status; where a code is answered with another status, the endpoint reference says so. Codes that also exist in Converger Protocol v1 (WebSocket and SSE error frames) are spelled the same and mean the same thing; the Protocol column gives the frame's number.

CodeStatusProtocolMeaning
bad_request4001000Malformed request: invalid JSON, a missing or mistyped parameter, an invalid cursor or filter, a malformed UUID
invalid_watermark4001003The watermark could not be decoded (tenant API; the client API restarts from the beginning instead)
channel_inactive4002003The addressed channel is disabled. 403 when it is the channel of the presented Converger token
inbound_not_supported400-Inbound message to an outbound-only channel
unsupported_protocol4001002The WebSocket upgrade of /socket/converger/v1 offered only subprotocols the server does not speak; details.supported lists them
unauthorized4012000Missing or invalid credential: API key, channel token, Converger token, channel secret, inbound signature
token_expired4012001The Converger token expired; refresh it (POST /api/v1/converger/tokens/refresh before expiry) or issue a new one
token_revoked4012005The Converger token was revoked (its family or its end user, POST /api/v1/converger/tokens/revoke), was refreshed longer ago than the rotation grace period, or was refreshed again after that, which revokes its family. Get a new token from your backend. See client tokens
insufficient_permission4032004The Converger token's perms claim lacks the permission (read, write, typing) the operation needs; details.required names it
insufficient_scope403-The tenant API key lacks the scope (read, write, manage) the operation needs, or tries to issue a key with a scope it lacks; details.required names it (management API)
forbidden4032002Authenticated but not allowed: a conversation-bound token used for another conversation, a channel token where the tenant API key is required, an IP outside the admin allowlist, a failed webhook verification
origin_not_allowed403-POST /api/v1/converger/tokens/publishable from an origin that is not among the channel's allowed origins, or without an Origin header (publishable keys)
not_found404-Unknown route or resource, or a resource of another tenant or channel: existence is never revealed. Protocol v1 uses conversation_not_found (4001) for conversations
method_not_allowed405-The route does not accept this method
not_acceptable406-The Accept header excludes JSON
conflict409-The request conflicts with the current state of the resource
conversation_closed4094000The conversation is closed; reopen it before posting activities (ADR-0017)
not_failed409-Replay of a delivery (POST /api/v1/deliveries/:id/retry) or redelivery of a platform webhook event (POST /api/v1/webhook_events/:id/redeliver) that is not failed
precondition_failed412-The If-Match ETag of a management call does not match the resource's current one; details.current_etag has it. Fetch the resource again and retry (management API)
payload_too_large4133001A size limit was exceeded (upload size, raw inbound body)
unsupported_media_type415-The uploaded file's sniffed type is not allow-listed; details.content_type
invalid_message4221004The activity failed validation; details has the errors per field
validation_failed422-Another resource (a routing rule, a platform webhook subscription) failed validation; details has the errors per field
unsupported_channel_type422-The channel type does not support the operation (pull outbox, message templates)
invalid_channel_config422-The channel configuration lacks what the operation needs (for example waba_id to list templates)
upgrade_required426-/socket/converger/v1 only accepts WebSocket upgrades
rate_limited4293000Rate limit hit; wait Retry-After seconds (details.retry_after_ms)
internal5009000Unexpected server error
upstream_unavailable502-A provider (template listing) or storage backend (upload, download) call failed
unavailable5039001Retry later (Retry-After): the node is draining, the database is unavailable, or the activity could not be enqueued; details.reason says which

New codes may be added in a minor release (see the versioning policy): treat an unknown code like the generic code of its status (4xx as a client error, 5xx as retryable).

Details​

Codedetails
invalid_message, validation_failedErrors per field: {"text": ["should be at most 65536 byte(s)"], "attachments": ["attachment 0: contentType can't be blank"]} (the same map Protocol v1 puts in error.data.details)
rate_limited{"retry_after_ms": 830} (CORS does not expose Retry-After to browsers on another origin; this is readable)
insufficient_permission, insufficient_scope{"required": "write"}
precondition_failed{"current_etag": "\"q1Vw..\""}
unavailable{"reason": "draining" | "database_unavailable" | "delivery_enqueue_failed", "retry_after_ms": 1400} (retry_after_ms absent for delivery_enqueue_failed)
unsupported_protocol{"supported": ["converger.v1", "converger.v1+json", "converger.v1+msgpack"]}
payload_too_large{"max_bytes": 10485760} for uploads
unsupported_media_type{"content_type": "application/x-msdownload"}
othersnull

Validation example:

{
"error": {
"code": "invalid_message",
"message": "Invalid message",
"details": { "type": ["is invalid"] },
"request_id": "GBHDfGZ0bW5Sl2YAAAEB"
}
}

Retrying​

Retry rate_limited and unavailable after Retry-After (or details.retry_after_ms), and internal / upstream_unavailable with backoff. Activity writes are safe to retry with an x-idempotency-key (idempotency). Do not retry other 4xx codes unchanged.

Not covered by the envelope​

  • Successful provider handshakes and provider-specific acknowledgements of inbound webhooks (TwiML, Slack challenges, Meta's hub.challenge echo) keep the body the provider expects.
  • The Server-Sent Events stream and the WebSocket endpoints report errors after the connection is established as Protocol v1 error frames (Protocol v1, section 12). Refusals before that (bad token, draining) are HTTP responses with the envelope.
  • /metrics (Prometheus text) and /health/* (their own JSON status body), and the HTML admin and portal pages, including their CSV exports.

Migrating from the earlier shapes​

Before #45 the body depended on where the error was raised. HTTP statuses did not change; only bodies did:

BeforeNow
{"error": "Missing x-channel-token header"}bad_request, the string in message
{"errors": {"detail": "Not Found"}} (and Forbidden, Unauthorized, Bad Request)not_found (forbidden, unauthorized, bad_request)
{"errors": {"text": ["..."]}}invalid_message / validation_failed, the map in details
{"error": {"code": "Unauthorized", "message": "..."}} (client API)unauthorized, token_expired or channel_inactive (the code is now snake_case)
{"error": "Unauthorized: Invalid token"} (tenant API)unauthorized, message without the Unauthorized: prefix
{"error": "conversation_closed", "detail": "..."}conversation_closed
{"error": "not_failed", "detail": "..."}not_failed
{"error": "Too many requests. Please try again later."}rate_limited
{"errors": {"detail": "Service Unavailable", "code": "database_unavailable"}}unavailable, details.reason: "database_unavailable"
{"error": "unsupported WebSocket subprotocol", "supported": [...]}unsupported_protocol, details.supported
Forbidden (plain text, admin IP allowlist)forbidden
empty 503 / 403 bodies of refused socket connectionsunavailable / forbidden