Skip to main content

Tokens & DPoP

Your application authenticates to the gateway with a phantom token — a capability token minted for one agent. It is short-lived, scoped, and revocable, and it stands in for the real provider key (which never leaves the server).

Token shape

  • kp_ — the phantom-token family prefix. It is disjoint from every other KnoxCall credential: kc_ (OAuth 2.1 access), kr_ (OAuth 2.1 refresh), tk_ (tenant API key) and AKE (enterprise access key). See the 2026-09-01 changelog entry — this prefix changed, with no compatibility window.
  • envlive or test, e.g. kp_live_a_… and kp_test_a_….
  • kind — the capability class (below).
  • The trailing segment is a CRC over everything before it, so a mistyped token is rejected without a database lookup.
  • The token is validated by a capability HMAC bound to your tenant master key, so a token minted for one tenant can’t be replayed against another.
The environment segment is enforced, not cosmetic. A kp_test_ token resolves only sandbox routes and a kp_live_ token only Live ones — the segment is part of the route lookup on the primary route and on every failover route, so a 5xx cannot become a way for a Test token to reach a Live upstream. A request whose environment cannot be proven is refused rather than assumed to be Live.

Token kinds

One-shot tokens and rejected requests

A one-shot token is consumed at the END of authentication — after the token hash, expiry, capability HMAC, IP/CIDR window and DPoP proof have all passed, so a request rejected by any of those does not burn it. It is consumed before any gateway policy runs. A one-shot call that is then refused by the model policy, the tool allowlist, the prompt firewall, the budget or the per-call token cap has still spent the token: you get the 4xx explaining the refusal, and retrying the same token returns 401 oneshot_already_used instead of the original reason. That ordering is deliberate. If the consume waited until after policy, two concurrent replays of one token would both pass authentication, both run the pipeline and both reach the provider — spending real money twice — and the loser would only be told afterwards. A token burned by its own rejected request costs one re-mint; a token replayable during policy evaluation is not single-use at all. Practical rule: mint a one-shot per attempt, not per intent. If your client retries, mint again.

Scoping

A token carries a capability scope — a small JSON object, HMAC-signed with your tenant master key, so it cannot be edited without the key. Every key below is enforced; the two marked at authentication are checked by the auth middleware, the rest need the resolved request (its model, tools and route) and so are enforced in the data plane, before the upstream is dialled. Expiry is separate from the scope: every token can carry a TTL and an expired token is rejected at the edge. A malformed restriction is a refusal, never a pass. {"models": "claude-haiku-4"} (a string where a list is required) and {"models": []} (a list that restricts to nothing) both refuse the request with 403 malformed_token_scope, because treating either as “unrestricted” would silently invert the most restrictive value into the most permissive one.
There is no mint API that sets a scope yet. POST /v1/ai-gateway/agents/{agentId}/tokens and its admin twin both stamp an empty scope deliberately: neither offers a parameter to set one, and an earlier revision guessed a providers restriction from the model name — which mislabelled every Gemini, Mistral and Cohere agent and would have refused the token’s own traffic. An empty scope means “no token-level restriction”, which is the truth.The one path that produces a scoped token today is an OIDC binding, where you author the scope on the binding. A binding whose scope narrows nothing is now refused at write time, because an unnarrowed scope mints an unrestricted token.An explicit scope parameter on mint is planned and not shipped; it needs the Postman and five-SDK fan-out that every /v1 change requires.
Revoke a token at any time from the Tokens tab on the gateway detail page, or with DELETE /v1/ai-gateway/agents/{agentId}/tokens/{tokenId}; revocation takes effect immediately.

Gateway-level (agentless) tokens

Not every phantom token belongs to an agent. A token can be minted at the gateway level — it has no agent_id — which is how a credential is issued for a whole gateway rather than one agent’s traffic. Because such a token is invisible to every agent-scoped route, /v1 carries two endpoints that address the gateway instead:
  • GET /v1/ai-gateway/gateways/{gatewayId}/tokens — lists the gateway’s tokens, agent-scoped and agentless.
  • DELETE /v1/ai-gateway/gateways/{gatewayId}/tokens/{tokenId} — revokes any of them, including an agentless one that no agent endpoint can reach.
There is no POST here: nothing mints a gateway-level token on request. The one thing that produces one is the OIDC exchange when the request carries an RFC 8707 resource naming an MCP server — the resulting token is kind tool, confined to exactly that one MCP resource by its capability HMAC, and refused outright on /v1/ai, which requires an agent it does not have. If you find an agentless token you cannot account for, this is the endpoint that revokes it.

DPoP (sender-constrained tokens)

Set dpop_required: true on a token to make it sender-constrained per RFC 9449. The client generates a key pair and sends a DPoP proof JWT on every request; the gateway binds the token to that key’s thumbprint (jkt) and rejects the token if presented without a matching proof. This means a leaked token is useless without the corresponding private key — the single most effective mitigation for the “unscoped env-var key” problem that dominates agent deployments. DPoP nonces are tracked in a durable, cross-worker store so replayed proofs are rejected even behind multiple workers.

OIDC workload federation (CI without static secrets)

CI systems and workloads can exchange a short-lived OIDC id_token (e.g. a GitHub Actions token) for a scoped phantom token via RFC 8693 token exchange, so no long-lived secret is stored in the pipeline:
An OIDC binding (configured on the agent’s Federation section) pins the trusted issuer, audience, and required attribute conditions (for example, a specific repository), and mints a token only when the presented id_token matches. Subject tokens are single-use — a replayed id_token is rejected.

Impossible-travel detection and step-up

Set a gateway’s geo_velocity_action to catch a token used from two places too far apart to be the same holder (a leaked credential in use elsewhere):

What block actually does

block never revokes a token and never fails the request that triggered it. The geo lookup is an external call made off the hot path, so the current request is already on its way upstream by the time the classifier has an answer. What block does is set step_up_required on the token row, and the next presentation of that token is refused at authentication:
Match on the code field (or the X-Auth-Failure header), not the message. error carries the same value — see Errors. Before 2026-09-08 this refusal answered {error: "Unauthorized", code, reason}; code is unchanged, and reason is now error_description. The refusal happens before the agent is loaded, so no upstream call is made and nothing is charged. This is the safe form of “block”, and the reason it is not revoke: a false-positive impossible classification — a VPN hop, a mobile carrier’s NAT, a stale geo-IP row — would otherwise lock a legitimate user out of a credential they cannot get back. Forcing re-authentication is something the real holder can do and a thief holding only the bearer token cannot.

Recovering from a step-up

Two ways back:
  1. Mint a fresh token for the agent and reconfigure the consumer. This is the correct response when you cannot rule out that the token leaked.
  2. Clear the flag, when you have confirmed a false positive (a shared machine key used across regions, a CI runner that moved provider):
    Owner/admin only, tenant-scoped, and it only affects a token that is currently flagged — a call for any other token returns 404. The token resumes working immediately with its original scope and expiry.
Clearing a step-up is available on the tenant admin API only. There is no /v1 equivalent and no button in the admin UI yet, so today this is an API call or a re-mint. Prefer the re-mint unless you are confident the flag was spurious — clearing it puts a token back in service that the gateway told you it could not account for.

Operational notes

  • The check skips the geo lookup entirely when the source IP is unchanged, which short-circuits the overwhelming majority of requests, and the per-gateway action is cached for 30s.
  • Every stage fails open on error: a geo provider timeout, an unresolvable IP or a failed write costs one comparison, never access. A gateway whose action cannot be read is treated as off.
  • The first request from a token has no prior sample to compare against, so a token is never flagged on its first use.
  • Because the geo lookup is an external call, enable it deliberately (and pair it with a local geo database at high request volumes).
  • warn and block both emit ai_gateway.geo_velocity_anomaly, with severity scaled by how implausible the travel is. Watch it with an alert rule on that audit action.
Phantom tokens do not refresh, and there is no refresh-token rotation. An earlier revision of this page said a rotator and an ai_gateway.refresh_theft event existed in the codebase but were unserved. Both were deleted — the table they read was never written by anything, so a reviewer grepping for the detector would have found a wired dispatcher and concluded the detection existed. Reporting a signal that cannot fire is worse than reporting none.A phantom token expires and you mint another. Mint with a short TTL (a token with no explicit expiry now defaults to 30 days, clamped between 60 seconds and 90 days), and for CI use the OIDC exchange — which mints a fresh short-lived token per run and refuses a replayed subject token — rather than storing one.

Mint tokens

Mint from the Tokens tab on the gateway detail page, or programmatically via the control-plane mint endpoint. The plaintext token is shown once at mint time — store it securely.