Prima Get an API key

Prima API

Errors

One OpenAI-shaped envelope on every gateway path, and the three status codes that do not mean what they look like.

Live Built and working today on the surface this card describes.

What it does

Every error under /api/prima/v1 returns {"error": {"message", "type", "param", "code", "request_id"}}. error is an object, which is what a stock OpenAI SDK reads error.message off.

The shape is deliberately scoped to the gateway prefix. Other paths on the same host keep a different envelope, and there is a test asserting that, because changing it everywhere was the easy wrong fix.

request_id is the field to quote when reporting a problem.

Inputs and outputs

Envelope {"error": {"message": "…", "type": "…", "param": null, "code": "…", "request_id": "…"}}
401 type: invalid_request_error, code: invalid_api_key. Missing or invalid key, and deliberately one message for an unknown key, a revoked key and a key belonging to another tenant. See the authentication card for the Authorization header trap.
402 type: insufficient_quota, code: insufficient_credit. The balance will not cover the request. The top-up URL is text inside message, not a field of its own, and the balance is not in the envelope at all. Read it from /usage.
404 on a /credits* path Billing is not enabled on this deployment yet, not a bug and not "no tiers". Prima's route-miss envelope carries code: null.
403 code: insufficient_scope: the key was found and does not carry ai. This is the read-scope trap, and it is a 403 on every route, GET /models included. scope_not_recognised is a key carrying a wildcard the gateway does not define. On the account-service paths a 403 carries code: AUTHZ_001 instead, and there are two to know in the message. session_required: a key was used on PUT or DELETE /credits/auto-topup or on a top-up with save_card: true, which take a Labs session only; send the session, the key is not being refused as a key. owner_required: the person behind the credential, the session's user or the key's on a plain top-up, is neither the workspace owner nor an admin, and the three /credits writes are theirs alone (A-1, decided 2026-09-11, built at alchemy_labs 3d21356 on w1-auto-topup). The two ask for different things: session_required is a credential failure, keep what the person was trying to save and send it with a session; owner_required is final for this person, show "ask the workspace owner or an admin", disable the write and do not retry.
400 on a bad body code: invalid_request, param naming the field, a fixed message that never echoes the value. The earlier in-Labs gateway answered 422 for validation; the standalone service answers 400.
429 Two codes that ask for opposite things. rate_limit_exceeded says wait. spend_cap_exceeded says the period's hard cap is reached: raise it or wait for the period to roll over. A client that cannot tell them apart retries forever against a wall.
503 identity_provider_unavailable with a Retry-After: a Labs session token the deployment could verify was presented where only a key works. One it cannot verify is the 401 above instead; see the authentication card. credits_unavailable: the ledger could not be reached and the money path failed closed.
502 upstream_error, type: api_error. Also what a prompt that is too long returns.
On /credits* and /accounts* Same envelope, different process, different code. Those paths are served by the account service, whose code is its own status-class token (VAL_001, AUTH_001, AUTHZ_001, RES_001, RES_003, RATE_001, SRV_001) and whose type includes permission_error, not_found_error and rate_limit_error. The machine token (session_required, owner_required, no_card_on_file, cap_below_amount, unknown_credit_tier, topup_not_configured, invalid_cursor, no_workspace) is inside message, as the text 'error': '<token>'. Match it there. A body that fails validation on those paths is FastAPI's default {"detail": [...]}, the one shape on the prefix that is not this envelope.

Configuration

Nothing to configure The envelope is not selectable and does not vary by client.

Limits

  • A context overflow returns 502, not 400 context_length_exceeded. A 5xx means "my fault, try again", so an oversized prompt is indistinguishable from an outage and a retry loop will retry something that can never succeed.
  • Two envelope shapes have been seen in the wild across the estate, and during a 25-minute outage on 2026-08-03 a 502 came back with an entirely empty body, which throws a raw parse error in every stock client. Treat an unparseable body as an error, not as a response.
  • Error messages are sanitised so no vendor name reaches a caller, but that sanitising is not uniform across every path. One path was measured emitting an upstream status code and a byte count to a user-facing surface.
  • There is no Retry-After on a 5xx.

Example

the shape of a 401

{
  "error": {
    "message": "…",
    "type": "invalid_request_error",
    "param": null,
    "code": "invalid_api_key",
    "request_id": "…"
  }
}

Source of truth

  • philosophers_stone/platform/primaapi.md § OpenAI compatibility (the envelope, and why it is scoped to the gateway prefix)
  • philosophers_stone/platform/primaapi.md § Context overflow returns 502, not 400
  • philosophers_stone/platform/primaapi.md § Open items (one envelope, empty body on the 2026-08-03 outage, vendor-name leakage)
  • prima/prima/errors.py (the envelope, and the type and code behind every status on this card: 401 `invalid_request_error` / `invalid_api_key`, 402 `insufficient_quota` / `insufficient_credit` with the top-up URL in the message, 429 `rate_limit_exceeded` and `spend_cap_exceeded`, 502 `upstream_error`, 503 `service_unavailable`)
  • prima/prima/auth/keys.py (403 `insufficient_scope` and `scope_not_recognised`)
  • prima/prima/auth/sessions.py (503 `identity_provider_unavailable` with `Retry-After`)
  • prima/prima/api/v1.py (503 `credits_unavailable`, the money path failing closed)
  • prima/prima/app.py (the route-miss 404 carrying `code: null`; a validation failure is 400 `invalid_request` with `param` and no echo of the input)
  • alchemy_labs/backend/app_fastapi.py (`http_exception_handler`: on `/api/prima/v1/*` the account service answers the same envelope with its own `ErrorCode` in `code` and `str(detail)` in `message`; 403 is `permission_error` / `AUTHZ_001`)
  • alchemy_labs/backend/api/routers/primaapi_router.py, branch `w1-auto-topup` (`_require_labs_session`: the 403 `session_required` detail and where it is raised; `_require_billing_role`: the 403 `owner_required` detail and its hint, on the three writes after the session guard, since 3d21356)
  • philosophers_stone/operations/prima-onboarding-phase2-report.md § 5.A, the A-1 block (403 `owner_required` for a member who is neither owner nor admin, decided 2026-09-11)
  • philosophers_stone/operations/decisions/0036-prepaid-credits-auto-topup-and-balance-events.md § 7 (a `/credits*` path answers Prima's 404 until the ingress rules are applied)