A virtual key (ob_live_… or ob_test_…) is the credential an agent presents to the gateway. It is never a vendor credential and never an operator session token. See Virtual keys for what the key carries and how the gateway reads it.
Plaintext is returned once, in the mint response, and is never stored, echoed, logged, or re-served. Control stores only sha256(plaintext) plus the last four characters. A lost key is re-minted, never recovered.
Minting and revoking require the key.manage action (developer, admin, or owner). Listing requires workspace.read, which every role holds. A frozen workspace answers 409 on mint and revoke.

Key object

Returned by all three routes. The mint response adds plaintext.
string
required
Key id, prefixed key_. This is the id, never the secret.
string
required
ob_live or ob_test. The minted secret begins with this plus an underscore.
string
required
active, revoked, or expired. Computed at read time: a key past expires_at reports expired whatever the stored column says, and a revoked key reports revoked regardless of expiry.
string
required
The agent this key was minted for.
string
required
The last four characters of the plaintext. The only fragment of the secret control retains.
array
required
Model allowlist. ["*"] when unrestricted.
array
required
Tool allowlist. ["*"] when unrestricted.
string
required
full or progressive.
integer | null
required
Monthly spend cap in micro-USD, or null for no cap.
integer | null
required
Requests-per-minute limit, or null.
integer | null
required
Tokens-per-minute limit, or null.
array
required
Surfaces this key may use. Defaults to ["mcp", "llm"].
string | null
required
RFC 3339 UTC expiry, or null.
object
required
Free-form attributes carried into the key context the gateway evaluates. Defaults to {}.
string
required
RFC 3339 UTC.
string | null
required
RFC 3339 UTC when the key was revoked, otherwise null.

Mint a key

POST /api/v1/workspaces/{workspace_id}/keys Requires key.manage. Returns 201 with the key object plus plaintext. The mint and its audit row commit in the same transaction, so a key that exists always has a row naming who created it. No secret material reaches the audit detail — it carries agent_id, prefix, last4, tools_mode, and scopes.

Path parameters

string
required
Workspace id, prefixed ws_.

Body

string
required
1–128 characters. Names the agent this key belongs to.
string
ob_live or ob_test. Defaults to ob_live in a prod workspace and ob_test otherwise. A ob_test prefix is rejected for a prod workspace.
array
Model allowlist. Omitted or null stores ["*"].
array
Tool allowlist. Omitted or null stores ["*"].
string
default:"full"
full or progressive.
integer
Monthly spend cap in micro-USD.
integer
Requests per minute. Must be at least 0.
integer
Tokens per minute. Must be at least 0.
array
Omitted or null stores ["mcp", "llm"].
object
Free-form attributes. Omitted or null stores {}.
string
RFC 3339 timestamp. After it passes, the key reports expired.
string
required
The full secret. Present only in this response. Store it now.

Errors

After the commit, control writes the key context to the Redis hot path. If that write fails the response still succeeds: Postgres is the source of truth, and the gateway falls back to reading the key from control.

List keys

GET /api/v1/workspaces/{workspace_id}/keys Requires workspace.read. Returns every key in the workspace, revoked and expired ones included.

Path parameters

string
required
Workspace id, prefixed ws_.
array
required
Key objects, without plaintext.
This route is not paginated.

Revoke a key

POST /api/v1/workspaces/{workspace_id}/keys/{key_id}/revoke Requires key.manage. Returns 200 with the updated key object. Revocation is idempotent: revoking an already-revoked key leaves revoked_at unchanged and still answers 200. The key is resolved inside the tenant boundary — workspace_id is in the query predicate, not in a follow-up check — so another tenant’s row is never loaded.

Path parameters

string
required
Workspace id, prefixed ws_.
string
required
Key id, prefixed key_.

Errors

After the commit, control drops the key from the Redis hot path. A failed drop does not fail the request; the durable revocation stands and the gateway’s fallback read reflects it.