Prima Get an API key

Prima API

Authentication

The key goes in X-API-Key. A stock SDK sends Authorization anyway, and there is one line that stops it mattering on every path.

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

What it does

The gateway authenticates on X-API-Key. A gateway key is not a JWT, and until 2026-08-03 an Authorization: Bearer header the gateway could not decode was refused outright, so a developer following our own instructions sent a valid X-API-Key alongside the SDK's own Authorization header and got a 401.

Two fixes since. An undecodable Authorization header now falls through to X-API-Key when that header is present. And on the inference paths (/models, /chat/completions, /embeddings, /usage) a prima_pk_ key sent as Authorization: Bearer <key> with no X-API-Key at all is looked up as a key, since 2026-08-04. That is the shape a stock SDK produces when you pass it the key as api_key.

The billing and account paths under the same prefix (/credits*, /accounts*) are served by a different process and do not have the second fix: there, a key in Authorization alone is a 401, and the key has to be in X-API-Key. The sentinel below is the one shape that works on every path, which is why it is still the recommendation.

A Bearer token that does decode as a Labs session still wins over a key, so a real session is never downgraded to a key.

Whatever header carries it, acceptance is a SHA-256 hash lookup of the whole key, never a prefix match. A key that is found is then checked for the ai scope; a key without it is refused on every route, including GET /models.

Inputs and outputs

Header X-API-Key: prima_pk_…
SDK sentinel default_headers={"Authorization": ""} when constructing the client, so the SDK's own header is empty rather than wrong.
Scope required ai. A found key without it answers 403 with code: insufficient_scope and a message naming the scope. A key carrying admin, full or * is refused with 403 scope_not_recognised: the gateway defines no wildcard. See the keys and scopes card.
On failure 401 in the OpenAI-shaped envelope, type: invalid_request_error, code: invalid_api_key. One message for a missing key, an unknown key, a revoked key and a key belonging to another tenant.
GET /whoami What the gateway resolved: subject id, tenant id, auth method, scopes, and the state of its identity provider. Opaque ids and numbers only, so the output can be pasted into a support conversation. In the code; not yet measured on api.prima.li.

Configuration

Base URL https://api.prima.li/api/prima/v1
Where to put the key An environment variable or a file the process reads. Not a command-line flag, which lands in shell history and in the process table.
Labs session A Bearer token that decodes as a Labs session is accepted on the billing and account paths under the same prefix, with X-Workspace-ID naming the tenant when the person belongs to more than one workspace, and it is the only credential three of those paths take: PUT and DELETE /credits/auto-topup, and POST /credits/topup with save_card: true, answer 403 session_required to a key. The session has to be the workspace owner's or an admin member's; any other member is 403 owner_required. It is not accepted on /usage or the inference paths; which refusal it gets there depends on the deployment, see the limits.
Prefix prima_pk_. Older deap_pk_ rows exist in the account service's key table; whether one still authenticates on api.prima.li has not been checked.

Limits

  • A Labs session token is not accepted on /usage or any inference path, and which refusal it gets depends on whether the deployment holds the Labs signing key. A token the gateway can verify is refused with 503 identity_provider_unavailable, type: api_error, with a Retry-After header, because the introspection half of that seam was never built on the Labs side. A token it cannot verify, unknown kid or a signature that does not check, is undecodable and falls to the same 401 invalid_api_key as any unknown credential. The wiki records that the standalone service cannot yet verify a Labs JWT, which makes the 401 the likelier answer; neither has been measured on api.prima.li. Call those paths with a key.
  • Authorization: Bearer <gateway key> alone works on the inference paths and fails with a 401 on /credits*. A client that uses the same header everywhere will see billing calls fail and inference succeed. Send X-API-Key.
  • The two credentials are not interchangeable on the billing writes. A key reads /credits/* and opens a plain top-up; changing or deleting the auto top-up instruction, or banking a card, is 403 session_required with a key and needs the session, since 2026-09-11 on the auto top-up branch. The credential is not enough by itself: the three /credits writes, a plain top-up included, are for the workspace owner or an admin member, and anyone else is 403 owner_required, checked on the key's own user when the write came with a key, so the owner's key still tops up and a member's does not. Decided 2026-09-11 and built at alchemy_labs 3d21356 (_require_billing_role), not deployed. See the credits card.
  • There is no token revocation for Labs session tokens on the estate today, and an access token lasts 60 minutes. Signing out does not end a gateway session. Revoke a key you are finished with rather than relying on a sign-out.
  • A key's IP allowlist is enforced, and it fails closed: a key with allowed_ips set is refused with 401 whenever the peer address is missing or not in the list. Behind the proxy the gateway sees only the proxy's address, so setting the column locks the key out on every request. Leave it unset until a trusted-proxy policy exists.

Example

the sentinel, in the OpenAI Python SDK

from openai import OpenAI

client = OpenAI(
    base_url="https://api.prima.li/api/prima/v1",
    api_key="unused",
    default_headers={
        "X-API-Key": PRIMA_API_KEY,
        "Authorization": "",
    },
)

Source of truth

  • philosophers_stone/platform/primaapi.md § OpenAI compatibility (X-API-Key, the SDK sentinel, the Bearer fall-through)
  • philosophers_stone/platform/primaapi.md § The auth seam, as built (no working revocation, 60-minute access token)
  • philosophers_stone/platform/primaapi.md § Open items (`prima_pk_` and `deap_pk_`; Labs' half of the auth seam does not exist in any branch, so only the `X-API-Key` path is complete)
  • prima/prima/seams/identity.py (`resolve`: Bearer first, an undecodable Bearer falls through to `X-API-Key`, and a `prima_pk_`-shaped Bearer with no `X-API-Key` is looked up as a key since 2026-08-04; `_resolve_key`: hash lookup, never prefix)
  • prima/prima/auth/keys.py (`AI_SCOPE`; `WILDCARD_SCOPES` are denied with 403 `scope_not_recognised`; a key without `ai` is 403 `insufficient_scope`)
  • prima/prima/errors.py (`authentication_error`: 401, `invalid_request_error`, `invalid_api_key`, one flattened message)
  • prima/prima/seams/identity.py (`_resolve_jwt`: an unknown `kid` or a failed signature check is undecodable, and with no key that is a 401 `invalid_api_key`; the 503 is reached only after the token verifies)
  • prima/prima/auth/sessions.py (`_unavailable()`: 503, `api_error`, `identity_provider_unavailable`, `Retry-After: 30`, reached from the federation path after a verified token)
  • prima/prima/auth/keys.py (`check_key`: `allowed_ips` is enforced and fails closed when the peer address is unknown or not listed)
  • prima/prima/api/auth.py (`GET /whoami`; `_client_ip` is the peer address only, no `X-Forwarded-For`, and why `allowed_ips` must stay NULL behind the proxy)
  • alchemy_labs/backend/services/primaapi/identity.py (`resolve_gateway_identity`: on the Labs-served paths an undecodable Bearer falls through only when `X-API-Key` is present; alone it is 401)
  • alchemy_labs/backend/api/routers/primaapi_router.py, branch `w1-auto-topup` (`_require_labs_session`: `auth_method == "jwt"` on the three billing writes, else 403 `session_required`; `_require_billing_role` after it since 3d21356: 403 `owner_required` on the same three writes, a plain top-up with the key included)
  • philosophers_stone/operations/prima-onboarding-phase2-report.md § 5.A, the A-1 block (owner or admin only on every billing write, 403 `owner_required` otherwise; decided 2026-09-11)
  • primacode/packages/primacode/src/config.ts (key resolution: env var, then a mode-600 file, never a CLI flag)