Console
API keys
The ordinary way to reach the Gateway — a desktop client, a script, your own service. Org-owned, scoped, revocable, and metered like everything else.
An API key is how a caller reaches the Gateway: a client on your laptop, a CI job, a service you run. Keys belong to an organization, not to a machine and not to a person, so they survive both. Two older credentials still authenticate — a per-agent gateway token and the platform agent secret, both minted before the agent-VM runtime was retired — but nothing issues new ones, and an API key is the only kind you can create.
Creating a key
In the Console, go to Operate → API Keys and press Create key. Give it a name, tick the scopes, optionally bind it to a legacy agent row, optionally set an expiry of 30 days, 90 days or a year. The secret is shown exactly once. What the key can then reach is on Models, the surface directly under this one.
Any member of the organization can see the list of keys, because nothing secret is stored to leak. Only an owner or an admin can mint one, because a key is spend authority.
What a key looks like
Nine characters of marker, then eight hex characters of selector, then forty-eight hex characters of secret. The prefix (lsk_live_a1b2c3d4) is not secret. It is safe to show in a UI, print in a log line, or paste into a support conversation, and it is how a key is identified after it has been issued.
The selector is why verification is fast: one indexed read on the prefix, then a single constant-time hash comparison, rather than hashing every candidate row. The hash is plain SHA-256 rather than bcrypt or argon2 on purpose. A slow key-derivation function is the right answer for low-entropy human passwords; this is 192 bits from a CSPRNG, where there is nothing to brute-force and a slow hash would cost real latency on the inference path.
Scopes
A key can only do what it is scoped for, and the absence of a scope is a denial. New keys default to inference and usage:read. Asking for a scope that does not exist is rejected with the valid list rather than silently dropped.
| Scope | Allows |
|---|---|
| inference | Call the Gateway — chat completions and the model catalog |
| usage:read | Read this organization's requests, latency and cost |
| agents:read | Read agent state. Vestigial — the routes it guarded went with the agent-VM runtime |
| agents:write | Change agent state. Vestigial for the same reason |
Two of the four are worth being blunt about. The agents:* pair was written for a runtime that could be started, stopped and rebuilt, and that runtime has been retired — there is no machine left for them to control. They remain in the scope list so an existing key keeps its recorded grants rather than silently losing them, and a key minted today should carry inference and usage:read and nothing else.
Scopes can be edited after the fact, along with the name. Doing so does not change the secret.
Binding a key to an agent row
A key can optionally be bound to one agent_instances row. A bound key inherits that row — its configured model, its managed-or-BYOK mode, its plan tier and its billing attribution. The row must belong to the same organization, and that is checked at creation, so a binding cannot be used to reach another org's billing.
What the row is not, any more, is a machine. The agent-VM runtime was retired and agent_instances survives as the billing record that Gateway authentication reads on every call; columns like server_tier and region are still on it and no longer describe anything. Binding is therefore a way of inheriting an older account's configuration, not a way of driving something.
An unbound key is organization-level: managed inference, the organization's plan ceiling, model auto, and usage recorded against the org with no agent attached. The Console shows these as Organization in the Bound to column, and every key minted today is this kind.
Using a key
X-Lobstack-Client is optional. It is recorded on the request trace and groups your traffic in the Console, truncated to 200 characters. It is a header, so it identifies nothing and authorizes nothing — it is a label. When it is absent the user agent is recorded instead.
What comes back
Every response carries the routing and cost decisions in headers, so you never have to infer what happened.
| Header | Meaning |
|---|---|
x-lobstack-request-id | The trace id. Quote it and the exact request can be looked up, including why it failed. |
x-lobstack-model | The model that actually served the request. |
x-lobstack-tier | The tier the request was scored into. |
x-lobstack-complexity | The complexity score that chose that tier. |
x-lobstack-routed | Whether Token Intelligence served a different model than the one requested. |
x-lobstack-cost-usd | What you owe for this request, priced when it ran. Empty means unpriced, which is not the same as free. |
x-lobstack-savings-usd | Dollars saved against the baseline model. Empty when there is no baseline. |
x-lobstack-baseline-model, -reason, -usd | What the saving was measured against and why: “named” if you asked for it, “plan_ceiling” if you sent “auto”. Sent only when a baseline exists. |
x-lobstack-quota-meter | Which allowance the quota headers describe: spend, requests or legacy. |
x-lobstack-savings-pct | The tier-level estimate. Not a measured dollar figure. |
x-lobstack-priced | Whether the served model was in the catalog at all. |
x-lobstack-metered | Whether the ledger write succeeded. Metering is best-effort. |
x-lobstack-mode | managed (Lobstack's provider key) or byok (yours). |
x-lobstack-principal | Which kind of credential authenticated: api_key, gateway_token or agent_secret. |
x-lobstack-dropped-params | Present only when a parameter was dropped, e.g. a temperature the model would 400 on. |
Errors
Failures carry a class alongside the status, so a client can branch on whose problem it is without matching on message text.
| Class | Whose problem | Retry? |
|---|---|---|
| auth | The credential was rejected. Fix the key. | No |
| quota | Allowance exhausted. Upgrade, top up, or wait for the reset. | After the reset |
| validation | The request was malformed. Fix the call. | No |
| provider | The upstream model provider failed. Not your fault. | Yes |
| timeout | We gave up waiting. | Yes |
| internal | Our bug. | Report it with the request id |
A rejected key is always a 401, whatever the reason. Internally the verification distinguishes malformed, unknown, revoked and expired, and the secret is compared before the lifecycle is checked so that a wrong secret cannot learn whether a given selector names a revoked key or no key at all.
Quota
API-key traffic is enforced against the organization's monthly allowance. Exhausting it returns 402 with a retry-after giving the seconds until the period resets.
Which counters arrive depends on the meter, so branch on x-lobstack-quota-meter first. A plan that includes dollars of model spend sends x-lobstack-quota-allowance-usd, -spent-usd and -remaining-usd and no request counters. BYOK and the older messages-per-month tiers send x-lobstack-quota-limit, -used and -remaining, plus -credits when you hold purchased credits; remaining includes them. x-lobstack-quota-resets is sent on any meter once the reset date is known. They are on every response, so a client can see the wall coming rather than hitting it. The three meters are set out on Pricing & plans.
Last used, rotation and revoking
The Console shows a Last used column so you can tell whether anything still depends on a key before you turn it off. It is throttled to one write a minute: the column exists to answer “is this still in use”, which a one-minute resolution answers just as well as a row update on every request, and a row update on every request would sit on the inference hot path. So treat the timestamp as accurate to the minute, not to the request.
Revoking is immediate and cannot be undone — issue a new key instead. Revoking twice is not an error, and does not move the timestamp that records when access actually ended. A revoked key stays listed in the Console under its own heading rather than disappearing, so its past requests remain attributable and the question asked right after a hurried revocation (“which key was that?”) still has an answer.
To rotate: mint the replacement, deploy it, confirm the old key's Last used has stopped moving, then revoke. Keys can also carry an expiry of up to ten years, and a credential with an end date is one fewer thing to remember to clean up.
Reading usage programmatically
A key with usage:read can query the same numbers the Console shows — identical arithmetic, so the two never disagree.
Group by day, model, key or agent, over 7, 14, 30 or 90 days. The response carries request and error counts, a failure breakdown by class, true p50/p95/p99 latency computed over raw values, and cost. A truncated flag tells you when a very large window means the totals are a floor rather than an exact figure. The same surface is described on the API monitor page.
Handling keys
Treat a key like a password. Keep it in an environment variable or a secret manager, never in source control, and never in client-side code a browser can read — a key in a web page is a key anyone who opens dev tools now has.
Issue one key per client rather than sharing one. That is what makes group_by=key answer “what caused this spike”, and what lets you revoke one thing without taking down everything else.
supabase/migrations/20260908_api_keys_and_request_traces.sql. On a deployment without it the surface says so rather than reporting an empty list.