CP·CP LEDGER
Documentation

Errors

The error envelope, the status codes we return, and what each one means.

Every failure this API returns uses one JSON shape, whatever went wrong and wherever it went wrong.

The envelope

JSON
{
  "error": {
    "code": 404,
    "message": "No route for GET /api/v1/nope",
    "metadata": { "error_type": "not_found" }
  }
}

Three rules hold across the whole surface:

  1. error.code repeats the HTTP status. Not an application-specific code, not a string — the same integer you already have on the response line. If you are reading a body you have already read a status, and they will agree.
  2. message is for a human. Log it, show it to a developer, do not branch on it. Its wording is not a contract and it is the part most likely to be reworded.
  3. metadata.error_type is for a program. It is the stable identifier, and it is what a switch should read. It is present whenever the failure can be classified, and metadata may carry extra members alongside it — a rejected parameter's name, a requested model id — depending on the failure.

Status codes

Each status has a default error_type, which a more specific failure may override. A 400 caused by an over-long prompt, for instance, reports context_length_exceeded rather than the generic invalid_request.

| Status | Default error_type | What it means here | |---|---|---| | 400 | invalid_request | The request was malformed, missing a required field, or exceeded a limit on the prompt. | | 401 | authentication | No credential, or one we do not recognise. | | 402 | payment_required | The account has no spendable credit, or the key's own spend cap is used up. | | 403 | permission_denied | The credential is valid and is not permitted to do this. | | 404 | not_found | No such route, model, or generation. | | 408 | timeout | The request did not complete in time. | | 409 | — | A conflict on a management resource. This status carries no canonical type. | | 410 | — | Permanently removed. Returned by exactly one endpoint, a retired purchase route. | | 412 | precondition_failed | A precondition on the request was not met. | | 413 | payload_too_large | The request body exceeded the size limit. | | 422 | unprocessable | Well-formed, but not something we can act on. | | 429 | rate_limit_exceeded | Too many requests. Carries Retry-After and X-RateLimit-* when it comes from us rather than from a provider. | | 500 | server | Something failed on our side. See the note on masking below. | | 502 | provider_unavailable | The chosen provider was down, or returned nothing usable. | | 503 | provider_overloaded | No endpoint could satisfy the request, or the provider was saturated. | | 504 | timeout | The provider timed out. | | 524 | timeout | The request timed out at the edge before we answered. | | 529 | provider_overloaded | The provider is temporarily overloaded. |

Of these, the four below were run against the live deployment while this page was written. The rest are the statuses the gateway is built to return; treat the table as the set your client should handle, and the samples as the ones we can show you verbatim.

Captured responses

400 — a required field is missing. A completion request with no model:

JSON
{"error":{"code":400,"message":"`model` is required.","metadata":{"error_type":"invalid_request"}}}

401 — no credential.

JSON
{"error":{"code":401,"message":"Missing Authentication header","metadata":{"error_type":"authentication"}}}

403 — valid credential, wrong class. An inference key reading the balance:

JSON
{"error":{"code":403,"message":"Only management keys can perform this operation","metadata":{"error_type":"permission_denied"}}}

404 — a model the catalogue does not hold. Note the extra members alongside error_type: they name what was asked for and why the lookup ended.

JSON
{"error":{"code":404,"message":"No endpoints found for nonexistent/model.","metadata":{"requested":"nonexistent/model","resolved":"nonexistent/model","reason":"model_not_in_catalogue","error_type":"not_found"}}}

An unknown path returns the same status through a different route, naming the method and URL:

JSON
{"error":{"code":404,"message":"No route for GET /api/v1/nope","metadata":{"error_type":"not_found"}}}

500 responses are masked

A 500 always reads Internal Server Error and carries no detail about what actually failed or which provider was involved. The real cause is written to our logs against the request id, and the request-id response header is how you point us at it. Do not parse a 500 message for meaning — there is none in it by design.

Failures during a stream

A streaming request that has already begun sending data cannot be given a 4xx or 5xx status: the status line went out with the first byte and said 200. So a failure after that point arrives in the stream instead, as one final chunk carrying an error member and finish_reason: "error", after which the stream ends.

The consequence for a client is the part worth internalising: an HTTP 200 on a streaming request does not mean the generation succeeded. A client that only checks the status and concatenates deltas will silently treat a failed generation as a short one. Check the last chunk for an error member. On this shape metadata.error_type is always present — an unclassifiable failure reports unmapped rather than omitting the field.

We are not printing a sample of that chunk here, because inducing a mid-stream failure on a live deployment is not something these docs can do read-only, and every sample on this site is one that was actually run.

Rate limits

Two budgets apply to /api/v1, and both are counted in a fixed window:

| Budget | Limit | What it is for | | --- | --- | --- | | Per API key | 120 requests / minute | The ceiling that governs a single caller. | | Whole endpoint | 6000 requests / minute | A shared backstop against a flood. Normal use never reaches it. |

A refusal from either budget is a 429 with error_type rate_limit_exceeded, and it carries four headers describing the budget that actually refused:

HTTP
x-ratelimit-limit: 120
x-ratelimit-remaining: 0
x-ratelimit-reset: 1786765134
retry-after: 59

x-ratelimit-reset is a unix timestamp in seconds; retry-after is a count of seconds to wait. Prefer retry-after — it is unambiguous. These headers appear on 429 responses only, not on successful ones.

Two details worth knowing before you tune a client around this:

  • The limit is charged before your key is checked. A request with a bad or expired key still spends budget. A retry loop hammering a rotated key will earn a 429 on top of its 401s.
  • Reading the catalogue counts. GET /models and friends are unauthenticated, but a request that carries an Authorization header is still charged to that key. Poll the catalogue without one if you are polling it often.

Retrying

429, 502, 503, 504, 524 and 529 are the transient set — retry with backoff. A 429 raised by the gateway carries Retry-After; honour it rather than picking your own interval.

400, 401, 403, 404, 413 and 422 will not improve on retry. Fix the request or the credential.

402 means the account is out of credit; see Credits and usage.