PlanCover Docs
API

Errors and rate limits

The OpenAI error envelope, every status code /v1 returns, the rate limits, and what is safe to retry.

Every check, from the key and the request to the limits and the database, runs before the turn starts. So a refused call is a real HTTP status, and no turn has run. Only a failure after the turn started is different: a frame on the stream, or a 5xx that says not to retry.

The error envelope

Every error under /v1 uses OpenAI's envelope, so OpenAI-style SDKs raise their usual exceptions. All four keys are always present; param and code may be null.

{
  "error": {
    "message": "This field is not supported.",
    "type": "invalid_request_error",
    "param": "response_format",
    "code": "invalid_request"
  }
}

Every response, error or not, carries an x-request-id header. Quote it when reporting a problem.

An error body never echoes what was sent: a message the member typed does not come back in a validation error.

Routes outside /v1 (/auth/*, /documents/*, /chat/*) keep the native envelope.

Status codes

StatustypecodeWhenOpenAI Python raises
400invalid_request_errorinvalid_requestThe body is invalid or unreadable, a refused field is set, or a message breaks a rule. param names the field, for example messages.0.contentBadRequestError
401authentication_errorinvalid_api_keyThe key is missing, unknown, expired, revoked or for the other environmentAuthenticationError
401authentication_errorunauthorisedA member session was sent but is empty or malformedAuthenticationError
403permission_errorpermission_deniedThe key's role may not call /v1, or a tool message answers a call that is not waiting for an answerPermissionDeniedError
404invalid_request_errorunsupported_endpointAny other path or method under /v1, including POST /v1/responses and GET /v1/modelsNotFoundError
429rate_limit_errorrate_limitedOver a rate limitRateLimitError
503server_errorupstream_unavailableA dependency, the database included, is unavailable. No turn ranInternalServerError
500server_errorinternal_errorAnything else before the turn started. No detail is returnedInternalServerError

A 401 unauthorised is about the member session, not your key: mint a session with POST /auth/session and send its id, rather than rotating the key.

The 404 points at the endpoint to use:

{
  "error": {
    "message": "Use POST /v1/chat/completions (AI SDK: openai.chat() or @ai-sdk/openai-compatible)",
    "type": "invalid_request_error",
    "param": null,
    "code": "unsupported_endpoint"
  }
}

After the turn started

Once the turn is running, a failure can no longer be a clean refusal. Text may already have been sent, and an action may already have been attempted.

Streaming: one error frame in the envelope above, then the stream closes, with no [DONE]. Its type is always server_error. See Errors mid-stream.

Non-streaming: a 503 if the failure is worth retrying later, otherwise a 500, with type: "server_error" and the header x-should-retry: false. The OpenAI SDKs obey that header, so they do not replay the turn.

The code says what happened:

codeMeans
upstream_timeoutThe turn ran past TURN_TIMEOUT_SECONDS (120 seconds by default), or a step inside it timed out. The message says whether anything may have changed
upstream_unavailableA dependency failed during the turn
stream_interruptedThe turn ended without finishing
loop_limitThe assistant hit its internal step limit
internal_errorAnything else

Treat it as an open set: other codes, such as forbidden, can appear.

Before sending the message again, read the error's message: when an action may have gone through, it says to check first. A failed turn's streamed text may be missing from the server's history, so the assistant may not know what the member saw. Check the state of any action before asking again on the same conversation.

Rate limits

Two limits apply to a chat turn. Both use a fixed one-minute window.

LimitCountsDefault
Per keyEvery request the key makes, on every route, /auth/session included120 per minute, or the key's own limit if one was set (details)
Per memberChat turns for one member session20 per minute (RATE_LIMIT_MESSAGES_PER_MINUTE)

Anonymous calls (no session) count only against the key's limit.

Over either limit, the call is a 429 before anything runs, with two headers:

HeaderMeaning
Retry-AfterWhole seconds to wait, at least 1
retry-after-msThe same wait in milliseconds

What to retry

ResponseRetry?
429Yes, after Retry-After. The OpenAI SDKs do this themselves unless retries are off
503 before the turnYes, with backoff. No turn ran
500 before the turnAt most once, then report it with the x-request-id
400, 401, 403, 404No. Fix the request, the key or the session
Error after the turn startedNot automatically. Show what streamed, then let the member ask again

Non-streaming calls need their own settings to stay safe; see Retries and timeouts.

On this page