Errors
One error envelope for HTTP, one error event for streams.
Failures are explicit. The alternative — an assistant that smooths over a failed tool call with a plausible sentence — is the single worst outcome for an insurance product.
HTTP errors
Every non-streaming failure returns the same envelope, carrying the request id so a report can be traced:
{
"error": {
"code": "http_error",
"message": "Not Found",
"request_id": "f792521fd91a42e49c90d1da3f1c94b4",
"details": null
}
}Responses are JSON with a real status. A 429 also carries Retry-After
(whole seconds) and retry-after-ms.
| Status | Code | Meaning |
|---|---|---|
401 | invalid_api_key | The API key is missing, unknown, expired, revoked, or for the other environment (pcai_live_ is prod only, pcai_test_ everywhere else) |
401 | unauthorised | The X-PlanCover-Session member session is missing where required, or malformed (a session id never expires), or a sign-in code is wrong or expired |
403 | permission_denied | The API key's role may not call this route |
403 | forbidden | Not permitted — another member's confirmation or conversation, or one no longer waiting |
422 | validation_error | The body failed validation; details lists the fields |
429 | rate_limited | Over the API key's or the member's limit. Wait Retry-After seconds |
503 | upstream_unavailable | A dependency, the database included, is unavailable. No turn ran; retry |
500 | internal_error | Anything unhandled |
A chat turn runs every one of these checks before its stream opens, so a refused turn is one of these responses and never a stream.
Stream errors
Once a stream has started, an HTTP status is no longer available. Failures
arrive as an error event and terminate the stream:
{ "code": "upstream_timeout", "message": "…", "retryable": true }| Code | Meaning | retryable |
|---|---|---|
upstream_timeout | A node exceeded its ceiling | Yes |
upstream_unavailable | A dependency failed or timed out | Yes |
stream_interrupted | The stream ended without a terminator | Yes |
loop_limit | The graph hit its recursion backstop | No |
unauthorised | A session check failed after the stream opened | No |
forbidden | Not permitted — including an approval that is not the caller's to give | No |
rate_limited | Too many messages for this member. Retry after a wait, not immediately | No |
internal_error | Anything unhandled | No |
The key, the session, the quotas and a confirmation are all checked before the
stream opens, so unauthorised, forbidden and rate_limited normally arrive
as the HTTP statuses above. As frames they mean the turn was refused part-way.
A failed tool call is not one of these. It completes the stream normally and
reports itself on tool.completed, with one of the
stable tool failure codes. An error
frame means the turn itself ended.
Partial answers
An error after some text has already streamed leaves that text on screen. The client marks the message as incomplete rather than deleting it — the member saw it, and pretending otherwise is more confusing than the failure.
What the member sees
Error messages name the problem and the recovery. They do not expose stack traces, internal identifiers or upstream vendor names.