Errors
HTTP status codes, error envelope, and error codes returned by the API.
Error envelope
Most errors return a JSON error object with a machine‑readable code, a human‑readable
message, and an optional details array, alongside the request meta:
{
"error": {
"code": "agent_unavailable",
"message": "Agent is currently disabled.",
"details": []
},
"meta": {
"request_id": "b7f0b9c2-1a2b-4c3d-8e9f-0a1b2c3d4e5f"
}
}A few failures bypass the envelope. Branch on the HTTP status code first, then read
error.code when the body carries an error object:
401Unauthenticated — a missing, malformed, revoked, or expired bearer key:{ "message": "Unauthenticated." }.403/404from ownership or route binding — a resource you don't own, or a UUID that doesn't resolve:{ "message": "This action is unauthorized." }.
Everything else uses the envelope, including validation (422 validation_failed), a missing
scope, an expired key, and a missing premium plan.
Validation errors
When a request body or query parameter fails validation, the API returns 422 with the standard
envelope, code: "validation_failed", and a details array naming each invalid field:
{
"error": {
"code": "validation_failed",
"message": "The content field is required.",
"details": [
{ "field": "content", "message": "The content field is required." }
]
},
"meta": { "request_id": "b7f0b9c2-1a2b-4c3d-8e9f-0a1b2c3d4e5f" }
}message repeats the first field error for convenience; iterate details — each entry is
{ field, message } — to surface every problem at once.
This is not Laravel's default { "message", "errors": { "field": [...] } } validation shape.
Read error.details[].field and error.details[].message.
Status codes
| Status | Meaning |
|---|---|
200 OK | Request succeeded. |
201 Created | Resource created. |
202 Accepted | Asynchronous job accepted (crawl started). |
204 No Content | Succeeded with no body (delete). |
401 Unauthorized | Missing/invalid key, or the key has expired. |
403 Forbidden | Not the owner, missing scope, or no premium plan. |
404 Not Found | The agent, conversation, message, webhook endpoint, source, or training job does not exist, or it does not belong to the agent in the path. Visitor send and action‑result also return 404 when the conversation was not created through this API. A resource that exists but you don't own returns 403, not 404. |
409 Conflict | An open public API conversation already exists for this visitor (conversation_conflict). |
422 Unprocessable Entity | Validation failed, the conversation is closed, or a subscription limit was reached. |
423 Locked | The agent is disabled and cannot accept the operation. |
429 Too Many Requests | Rate limit exceeded — see Rate Limits. |
502 Bad Gateway | An upstream dependency (storage or the training service) failed. |
Error codes
These appear as error.code in the envelope:
code | Status | Meaning |
|---|---|---|
token_expired | 401 | The API key passed its expiry date. |
unauthorized | 401 | Visitor identity_token signature is invalid, or aud does not match the agent. Expired, malformed, or incomplete tokens are 422 validation_failed. |
premium_required | 403 | Organization lacks an active premium/AppSumo plan. |
insufficient_scope | 403 | The key is missing the scope this endpoint requires. |
forbidden | 403 | The key's user has no organization, or is not the organization owner. |
resource_not_found | 404 | The conversation, message, webhook endpoint, source, or training job was not found for this agent — or visitor send / action‑result targeted a conversation that was not created through this API. |
conversation_conflict | 409 | This chat_user_id already has an open public API conversation on the agent. Widget/email threads do not count. details includes conversation_id. |
validation_failed | 422 | A request body or query parameter failed validation, including a missing or invalid identity_token. |
conversation_closed | 422 | The conversation is closed (visitor send, or PATCH response_mode on a closed thread). |
subscription_limit_reached | 422 | An agent‑count limit on the plan was reached. |
subscription_limit_exceeded | 422 | A training quota was exceeded, or a visitor send that would dispatch AI hit the message credit cap. Conversation message is always The organization has reached its message limit. |
premium_plan_required | 422 or 403 | A premium‑only model or setting was requested (422 on agents/sources). Conversation PATCH and human reply map the same case to 403. |
agent_unavailable | 423 | The agent is disabled. Message is Agent is currently disabled. |
upstream_failure | 502 | Storage upload, crawl, retrain, or status lookup failed upstream. |
Examples
403 insufficient_scope
{
"error": {
"code": "insufficient_scope",
"message": "Token is missing required scope: source:create",
"details": []
},
"meta": { "request_id": "b7f0b9c2-1a2b-4c3d-8e9f-0a1b2c3d4e5f" }
}409 conversation_conflict
{
"error": {
"code": "conversation_conflict",
"message": "An open public API conversation already exists for this visitor.",
"details": [
{ "field": "chat_user_id", "conversation_id": "3f1a9c2e-8b4d-4e6a-9c1f-2a3b4c5d6e7f" }
]
},
"meta": { "request_id": "b7f0b9c2-1a2b-4c3d-8e9f-0a1b2c3d4e5f" }
}422 conversation_closed
{
"error": {
"code": "conversation_closed",
"message": "This conversation is closed.",
"details": []
},
"meta": { "request_id": "b7f0b9c2-1a2b-4c3d-8e9f-0a1b2c3d4e5f" }
}423 agent_unavailable
{
"error": {
"code": "agent_unavailable",
"message": "Agent is currently disabled.",
"details": []
},
"meta": { "request_id": "b7f0b9c2-1a2b-4c3d-8e9f-0a1b2c3d4e5f" }
}Include the X-Request-ID response header (also in meta.request_id) when contacting support —
it lets us trace the exact request.