API errors
Every error response from an authenticated /api/* endpoint follows the same shape, defined by ApiErrorSchema in packages/shared/src/api/errors.ts:
{
"error": {
"code": "UNAUTHENTICATED",
"message": "Human-readable description",
"requestId": "req_...",
"details": { "optional": "per-error context" }
}
}
The code field is a stable SCREAMING_SNAKE_CASE identifier — safe to key off programmatically. The message is for humans and may change between releases. The requestId is included when the request reached the error handler and is useful when filing support tickets. The details object is populated only for errors that carry extra context (validation failures, plan limits).
Public /v1/public/* endpoints return a smaller version of this shape — just { "error": { "code": "..." } } — because the widget does not need human-readable messages and because these endpoints are CORS-unrestricted. The error codes still come from the same enum where they overlap.
Status codes
| Status | Code | When it happens |
|---|---|---|
400 Bad Request | VALIDATION_ERROR | Body failed Zod validation or required field missing |
401 Unauthorized | UNAUTHENTICATED | Missing, empty, or invalid Bearer token on /api/* |
402 Payment Required | PLAN_LIMIT_REACHED | Agency is at its plan limit for the resource |
403 Forbidden | FORBIDDEN | Token is valid but lacks access to the resource |
403 Forbidden | NO_AGENCY | Authenticated user has no agency attached to their profile |
404 Not Found | NOT_FOUND | Resource does not exist or is soft-deleted |
409 Conflict | CONFLICT | Duplicate resource (e.g., second bot for the same clientUrl) |
429 Too Many Requests | RATE_LIMITED | See Rate limits |
500 Internal Server Error | INTERNAL_ERROR | Server-side bug; retry with exponential backoff |
503 Service Unavailable | TEMPORAL_UNAVAILABLE | Background worker (Temporal) is unreachable |
503 Service Unavailable | UPSTREAM_UNAVAILABLE | A dependent upstream service is unreachable |
Validation errors
When Zod validation fails, the 400 response's details field carries the Zod issues so you can surface field-level errors in your UI:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request body",
"details": {
"issues": [
{ "path": ["clientUrl"], "message": "Invalid url" }
]
}
}
}
Plan limit errors
When the agency has hit a plan ceiling — bots, monthly messages, active actions — the server returns 402 Payment Required with code PLAN_LIMIT_REACHED. The message names the resource and the current limit. The correct response is to surface an upgrade nudge, not to retry.
Retries
5xxerrors: retry with exponential backoff (1s, 2s, 4s, 8s, 16s).429: wait exactlyRetry-Afterseconds, then retry once.4xxother than429: do not retry — fix the request.