Errors and rate limits
Error envelope
Every non-2xx HTTP response, and every streaming error event, uses one envelope:
{
"error": {
"code": "turn_already_active",
"message": "Conversation 68d9a84d-711c-4a55-a9dd-da6363e58e38 is currently being processed."
}
}code is the stable, machine-readable identifier — branch on it. message is a human-readable explanation intended for logs, not for end users. The full list of codes and their HTTP statuses is in Error codes.
Validation failures
Request validation failures (422 / invalid_request) add a details array listing every offending field, while message stays a readable summary:
{
"error": {
"code": "invalid_request",
"message": "Request validation failed with 2 errors: body.settings.language: Input should be a valid string; body.assessment.evidence[0].state: Input should be 'present', 'absent' or 'unknown'.",
"details": [
{
"location": "body.settings.language",
"message": "Input should be a valid string",
"type": "string_type"
},
{
"location": "body.assessment.evidence[0].state",
"message": "Input should be 'present', 'absent' or 'unknown'",
"type": "enum"
}
]
}
}details is present whenever the failure was field validation, under a different code on each transport: invalid_request on HTTP, invalid_payload on WebSocket. Every other code — including 422 codes raised after the body parsed, such as unknown_evidence — keeps the two-field envelope.
On HTTP, location starts with the part of the request that failed (body, query, path, header, or cookie) and bracket-indexes array positions. On WebSocket it is a dot-joined path prefixed with the frame's event type, with array positions as plain segments — conversation.create.conversation.assessment rather than body.assessment. Branch on the transport before parsing it.
message summarizes at most the first five entries and appends ; and N more beyond that, while details always lists every failure. Submitted values are never echoed back.
type (string_type, missing, enum, …) is stable and safe to branch on when you map server-side validation onto your own form fields.
Errors on the streaming transports
The streaming transports deliver the same envelope inside an error event:
{ "type": "error", "error": { "code": "message_too_long", "message": "..." } }A WebSocket connection stays open after an error event unless the server also closes the socket — see close codes. An HTTP NDJSON stream closes after a terminal turn event.
Errors you will handle most often
| Situation | Code | Status |
|---|---|---|
| A turn is already being processed for this conversation (HTTP only). | turn_already_active | 409 |
The user message is longer than max_message_length. | message_too_long | 413 |
| The interview already finished. | conversation_completed | 409 |
| The conversation was permanently terminated. | conversation_terminated | 409 |
| Seeded evidence refers to an unknown concept. | unknown_evidence | 422 |
| A summary was requested before a recommendation exists. | not_ready_summary | 409 |
Rate limits
Rate-limited responses carry RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and — when applicable — Retry-After headers. Successful responses on rate-limited routes also carry the first three, so a client can back off before being throttled.
Exceeding the limit produces rate_limit_exceeded (429). On the WebSocket transport, a connection rejected by the rate limiter receives an error event with the same code and is closed with 1008 before the handshake completes.