Virtual keys are the bearer tokens your applications present to Rolter’s gateway — one of the three credentials in play, alongside provider keys and the admin token (which key do I need?). They let you issue separate credentials to different callers, scope each credential to specific models, and revoke or expire them independently — without ever rotating your upstream provider API keys. Rolter stores keys as cryptographic hashes, so even if your config is leaked, raw key material is never exposed.
How authentication works#
Rolter accepts virtual keys in the same two ways that OpenAI and Anthropic do, so existing SDKs work without modification:
| Style | Header |
|---|---|
| OpenAI-style | Authorization: Bearer <key> |
| Anthropic-style | x-api-key: <key> |
On each request, Rolter hashes the presented key (with an optional server-side pepper) and compares it in constant time against the stored digest. If no match is found, or if the matching key is disabled or expired, the gateway returns 401 Unauthorized.
Keys are stored as hashes — Rolter never holds plaintext key material in memory after startup. Treat each key value like a password: record it at creation time, because you cannot retrieve it later.
Key configuration fields#
You declare virtual keys in rolter.toml under [[virtual_keys]]:
| Field | Type | Description |
|---|---|---|
key | string | The bearer token value clients present — stored as a hash |
name | string | Human-readable label for logs and dashboards |
models | list | Model names this key may access; empty list means all models allowed |
disabled | bool | Set true to revoke the key without deleting it (default: false) |
expires_at | RFC 3339 datetime | Optional expiry — the key stops working at or after this instant |
Example#
The production-app key above may only call the gpt-4o and claude routes. Any request for a model not in the list is rejected with 403 Forbidden.
Running without virtual keys#
If you remove (or never add) the [[virtual_keys]] section, Rolter runs open — any client can reach the gateway without presenting a key. This is convenient for local development and internal tooling behind a network perimeter, but you should always configure virtual keys before exposing the gateway to the internet.
An open gateway forwards all requests to your upstream providers. Anyone who can reach the gateway URL will consume your API quota. Always configure virtual keys in production.
Revoking and expiring keys#
You have two non-destructive ways to stop a key from working:
Set disabled = true on the key. The entry stays in your config for auditing but all requests using it are immediately rejected. Re-enable by setting disabled = false.
Set expires_at to an RFC 3339 timestamp. The key works until that instant and stops automatically. Useful for contractor access or short-lived integrations.
Minting a key from the dashboard#
Minting your own key takes the Member role in the project in scope. A Viewer sees Generate virtual key disabled, naming the role it takes, and an empty list says to ask an admin of the project to make you a member.
The Account & keys screen mints keys for you, and it asks for two things the config file leaves optional:
A name is required
The plaintext secret is shown exactly once, immediately after creation. A
key that arrives unnamed can never be identified again except by its prefix
and creation time — so the dashboard will not mint one, and neither will
the API: POST /api/v1/me/projects/{project_id}/virtual-keys rejects a
blank or whitespace-only name with a 400. Names are trimmed and capped at
64 characters.
An expiry is chosen, not skipped
The picker offers 7, 30, 60 and 90 days and defaults to 30. “Never expires” is still there — it is a legitimate choice for a key you have other controls over — but it is a deliberate pick that renders a warning, not what you get by leaving a field alone.
The API takes expires_in_days, not an instant: the server computes
expires_at from it, so a client with a wrong clock cannot mint a key that
outlives what you chose. Omitting the field is the only way to ask for a
key that never expires. 0 is refused — it reads like “no expiry” but
would mean “already expired”.
A spend or rate cap can be chosen at mint time
The same sheet can bound the key as it is made: a spend cap (an amount over a daily, monthly or total window) and/or a rate cap (requests and tokens per minute). Both are optional. “Give this contractor a key that can spend $50” is one decision in one place, instead of an unbounded key and a second screen you may not get to.
The API takes them as budget ({"limit_usd": "50", "period": "monthly"})
and rate_limit ({"rpm": 30, "tpm": 20000}) on POST /api/v1/projects/{project_id}/virtual-keys and on POST /api/v1/me/projects/{project_id}/virtual-keys. A cap is an ordinary
budget or rate limit scoped to the key,
written in the same transaction as the key, so a key chosen to be bounded
never exists without its bound, and a cap the gateway would misread (a
negative amount, a 7d window, a rate limit with neither number) is refused
with a 400 before any key is made. Edit or remove it afterwards under
Budgets & Limits, or through the
budgets and rate limits API.
The gateway enforces it, not merely records it: a request that arrives once the key’s spend has reached its cap is refused with a 402 naming the key, and a request over its rate cap with a 429. Deleting the key deletes its caps.
Rotation keeps the original deadline and the caps
Rotating a key replaces its secret; it does not renew the decision. The fresh key expires exactly when the one it replaces would have, and it carries the same spend and rate caps, so rotating a leaked key never hands back an unbounded one.
Before you press the button the sheet states the key’s effective reach — which project it belongs to, whether it is limited to specific models or can route to every model the project can reach, and the date it stops working. The secret is shown once, so that is the last moment the question “did I just mint something narrow, or something that can spend money against every provider?” has a cheap answer.
Expiry is enforced by the gateway, not merely recorded: an expired key is refused at the data plane with a 401 that does not say which key it was or when it lapsed.
Rotating a shared key#
A key an admin minted for a project (Governance → Virtual Keys) can be
rotated in place with the Rotate button on its row, or
POST /api/v1/virtual-keys/{id}/rotate. The rotation issues a new secret for the
same key: its models, providers, cache setting, attribution, limits, expiry
and history all carry over, so there is nothing to copy by hand and nothing to
miss. The new secret is shown once, like a mint.
By default the old secret stops authenticating at the next snapshot. To roll a
fleet out gradually, choose an overlap (1 hour, 24 hours or 7 days in the
dashboard; overlap_seconds in the API, up to 30 days). During the overlap the
old secret authenticates as the same key, with the same limits and attribution,
and it never outlives the key’s own expiry. The row names the old secret’s
prefix and when it stops working, and Revoke now
(DELETE /api/v1/virtual-keys/{id}/previous-secret) ends the overlap early.
Rotating again while an overlap is running replaces the old secret with the
latest one: a key holds at most one previous secret.
Rotation takes the same role as editing the key (admin on its project), and only for a shared key: a key a member minted for themself answers 409, because the secret would run as that member, so they rotate it from Account. Disabling a key also ends its overlap, so re-enabling it later does not bring the old secret back. Both the
rotation and an early revoke are recorded in the audit log as
virtual_key.rotate and virtual_key.revoke_previous_secret; only the secrets’
prefixes are recorded, and only peppered digests are ever stored.
A key you minted for yourself is rotated from Account instead, which issues a second key and disables the first.
After the key is minted#
The key is shown in a dialog, once, with a Copy button. Under it the dialog
shows the gateway URL and a first request written for that key: a curl call to
/v1/chat/completions on the gateway address, naming the first model the key may
reach, or the built-in fake-llm model when the key may reach every route. The
request reads the key from the ROLTER_API_KEY environment variable instead of
carrying it, so export the key as that variable and the request runs as shown.
The address is the public base URL a superadmin saved under Client Settings, shown to every signed-in role.
When none is saved, or your role cannot read it, the dialog asks for one instead
of showing a request: the dashboard’s own /gw proxy requires a dashboard
session, so a client outside the dashboard cannot call it.
Closing the dialog, by Done, the close button, Escape or a click outside it, asks “Close without copying?” while the key has not been copied, since the key cannot be shown again. Cancel keeps the dialog open. Once you have copied the key, closing asks nothing.
If the browser will not let the page use the clipboard, which is common on a
dashboard served over plain http, the dialog says the copy failed and selects
the key. The message stays until you press Copy again, and the key stays on
screen: copy the selection yourself with Ctrl+C or Cmd+C, which counts as a copy
for the question above. Select value selects it again.
Session affinity with x-session-id#
When you want all requests in a logical session to land on the same upstream target — for example, to benefit from a warm KV cache in a multi-turn conversation — send the x-session-id header:
Rolter uses this header as the affinity key in consistent_hash and pipeline strategies, and as a tiebreaker in cache_aware routing. The value can be any stable string that identifies the session — a user ID, conversation ID, or a combination.
Pair x-session-id with the consistent_hash or pipeline strategy on your route to maximize KV cache reuse and reduce time-to-first-token on multi-turn conversations.
Key pepper#
To harden virtual key storage, you can mix a deployment-wide secret into the digest. Set it via the environment variable ROLTER_KEY_PEPPER (preferred) or inline in rolter.toml:
With a pepper set, a leaked key digest is useless without the pepper value. Use a long random string and treat it like any other infrastructure secret.