VirtualKeyScope[] at any combination of ORGANIZATION, TEAM, PROJECT), a set of provider credentials, a routing policy, a cache policy, guardrails, blocked patterns, and budgets, everything the VK is allowed to do and how it is observed.
VKs replace raw provider keys in your applications. The provider keys themselves stay inside LangWatch (configured under Settings → Model Providers, the same surface you already use for evaluators and playground) and are never shared with downstream clients.
Quickstart, first request in 5 min
Create a VK, call
/v1/chat/completions, read correlation headers.Use your VK from Python, TypeScript
OpenAI, Anthropic SDK drop-in, just swap
base_url and api_key.Plug your VK into Claude Code, Codex, Cursor
Route coding-assistant traffic through the gateway for budget + audit.
Manage VKs from the terminal
langwatch virtual-keys create | list | rotate | revoke, scriptable.Format
vk-lw-{live|test}_<26-char-ULID>, 40 characters total.
vk-lw-, fixed prefix, grep-friendly, scans well in DLP, GitGuardian.liveortest, environment prefix. Prevents accidentally using a test key against production or vice versa.- 26-char Crockford-base32 ULID, time-prefixed, monotonic, renders sensibly in the dashboard and in your logs.
vk-lw-01HZX9) are visible in the list.
Server-side, the secret is stored as hex(hmac_sha256(server_pepper, secret)). Peppered HMAC-SHA256 is the right primitive for API keys (as opposed to passwords), the VK body already carries 130 bits of entropy as a ULID, so argon2id-style stretching would just add 50–100 ms of pointless latency on every cold resolve-key call. A short prefix is stored alongside for grep/lookup.
Creating a VK
Under AI Gateway → Virtual Keys click New virtual key and fill:| Field | Meaning |
|---|---|
| Name | Human label. Used in the UI and in langwatch.vk.name trace attribute. |
| Environment | live (default) or test. Reflected in the key prefix. |
| Scopes | One or more rows selecting where the VK is valid (ORGANIZATION / TEAM / PROJECT). The picker shows every scope the caller holds virtualKeys:manage on. Cross-scope VKs (e.g. TEAM “platform” AND TEAM “data-sci”) require manage on every named scope. |
| Routing policy | Optional. If unset, the VK falls back to the org’s default policy, ordered by fallbackPriorityGlobal then createdAt across every ModelProvider visible in the VK’s scope cascade. Pin a specific policy to override the default fallback order. |
| Models allowed | Optional allowlist. Blank = “any model the eligible ModelProviders support.” |
| Model aliases | Map of client-friendly names → provider/model. E.g. gpt-4o: azure/my-deployment. |
| Cache policy | respect (default), force, or disable. |
| Guardrails | Attach existing LangWatch evaluators as pre, post, stream_chunk hooks, the drawer has a dedicated picker section listing every project evaluator with executionMode = AS_GUARDRAIL. Fail-open toggles on pre and post directions surface the concrete 403, 50 ms, terminal-SSE enforcement shape so operators know what the toggle actually does. See Guardrails → Attaching guardrails to a VK. |
| Blocked patterns | Regex allow/deny for tool names, MCP servers, URLs. |
| Initial budget | Optionally attach a budget on creation. Budgets can always be added later. |
| Personal (principal) | Optional. Mark the VK as personal by setting principalUserId. Personal VKs cascade budgets through the principal first. The CLI device-flow self-mints a personal VK at ORG scope without requiring an explicit virtualKeys:manage grant. |
VK detail page
Clicking a row in the list opens the detail page (/gateway/virtual-keys/<vk_id>). Four sections, top-to-bottom:
- Header: name + action bar: Audit history (deep-links into
/settings/audit-log?targetKind=virtual_key&targetId=<vk_id>with a pre-filled filter chip showing only this VK’s events alongside any related platform actions), Edit, Rotate, Revoke. The Audit history deep-link is the fastest way to reconstruct “who changed this VK, when” during an incident. - Identity, Activity: id, prefix, environment, status, description, and humanised Last used + Created (hover for exact timestamp). Revision number shown when non-zero, increments on every
gateway.virtual_key.updatedaudit row. - Provider fallback chain: ordered rows, each showing provider icon + readable name + ModelProvider id + position badge (
primary,fallback-1,fallback-2, …). The list is computed from the bound routing policy (or the default policy if none is pinned). Clarifies at-a-glance which provider is in the driver’s seat and what falls back when it errors. - Configuration summary: compact read-only view (no drawer needed) of the runtime config that’s actually in effect:
- Tags: colored subtle badges, the same strings shown on the list row.
- Cache mode: badge (
respect,force,disable) + TTL on force. - Rate limits:
rpm+rpdoutline badges (or em-dash if unset). - Model aliases: expanded as
alias → targetpairs, up to 5 visible with “+N more” overflow. - Blocked patterns: grouped by dimension (
tools,mcp,urls,models) with red-tinted regex chips; click-through opens the edit drawer at that section. - Guardrails: count + direction breakdown (
N pre / M post / K stream_chunk), each clickable to the guardrail config.
- Usage (last 30 days): stat tiles (Total spend, Requests, Avg $/request), a 30-day filled area sparkline, Spend-by-model row, and the top 10 recent debits table (When relative, Model, Tokens in→out, Amount smart-decimals, Latency ms). Guarded on
virtualKeys:view+gatewayUsage:view.
Edit) or dedicated flows (Rotate, Revoke).
Rotation
Rotate a VK when you believe the secret may have leaked or as part of routine hygiene.- Open the VK in the list.
- Click Rotate secret.
- LangWatch mints a new secret and displays it exactly once.
- The old secret remains valid for a 24-hour grace window to let in-flight deploys update.
- After the grace window ends, only the new secret works.
vk_id, same ownership, same configuration, only the secret changes. Observability traces remain continuous across rotation.
Revocation
Revocation immediately invalidates the secret. There is no grace period on revoke. Gateway caches are invalidated within 60 seconds via the/internal/gateway/changes long-poll. If you need instant invalidation everywhere (e.g. incident response), restart the gateway pods, the next request that hits them will pull fresh state before serving.
Revoked VKs remain in the database (soft-delete) for audit. Their traces still appear in analytics.
Scoping
A VK belongs to exactly one organisation and carries one or more scope rows (VirtualKeyScope). Each row pins the VK to an ORGANIZATION, TEAM, or PROJECT. The scope set drives:
- Eligible provider set: union of every ModelProvider visible from any of the VK’s scopes (upward cascade). A VK scoped to PROJECT
demoseesdemo’s ModelProviders, the parent team’s, and the org’s. - Budget cascade: every applicable budget at any of the VK’s scopes is checked. Hard-block at any scope blocks.
- Trace attribution: the gateway tags spans with the most-specific scope match (PROJECT > TEAM > ORG).
langwatch.project_id on a span — see vk-scope-inheritance.feature and vk-config-bundle.feature.
Trace attribution on every request:
langwatch.virtual_key_idlangwatch.organization_id(always present)langwatch.project_id— resolution order: (a) the VK’s unique PROJECT-scope row, if it has exactly one; otherwise (b) the org’sinternal_governanceproject, where TEAM- and ORG-scoped VK spans land so all governance traffic surfaces in one filter view. If neither exists (older self-hosted deployments without governance), the attribute is omitted rather than 500ing the bundle.langwatch.team_id(present when the VK has a TEAM scope, or a unique team containing the resolved project)langwatch.principal_id(present whenprincipalUserIdis set on the VK)
Personal vs shared
Two flavours, distinguished by whetherprincipalUserId is set:
- Personal VK (
principalUserIdset): the VK is treated as that user’s personal credential. Budget cascade includes the user’s PRINCIPAL-scope budget first. Visibility is per-user by default;virtualKeys:viewOtherPersonalis required to inspect another user’s personal VK. The CLI device-flow self-mints one of these at ORG scope on first login. - Shared VK (
principalUserIdnull): service-account-style credential. Budget cascade runs only through scope-driven budgets. Visibility follows the standardvirtualKeys:viewperm at the VK’s scopes.
vk-lw-… format and the same multi-scope semantics; the principal column is orthogonal to scope.
Permissions
Acting on VKs requires the following permissions (2-segmentresource:action):
| Action | Permission |
|---|---|
| View list (own + scope-visible) | virtualKeys:view |
| View another user’s personal VK | virtualKeys:viewOtherPersonal |
| Create | virtualKeys:create (must hold :manage at every named scope when creating multi-scope) |
| Update config | virtualKeys:update |
| Rotate | virtualKeys:rotate |
| Delete, revoke | virtualKeys:delete |
| All of the above | virtualKeys:manage |
How the gateway resolves your VK
The first time the gateway sees a presented VK:- Calls
/internal/gateway/resolve-keyon the LangWatch control-plane → receives a signed JWT (15-minute TTL) with identity claims (vk_id,project_id,principal_id, etc.) plus arevisionnumber. - Calls
/internal/gateway/config/:vk_id(withIf-None-Match: <revision>on subsequent fetches) → receives the fat config (providers, fallback, guardrails, budgets). - Caches both in-memory (L1 LRU) and optionally in Redis (L2, shared across gateway pods).
- Subscribes to
/internal/gateway/changes?since=<revision>long-poll, on any mutation to any VK, the gateway invalidates and re-fetches.
- First request on a cold cache: 2-4 ms of control-plane overhead.
- Warm request after first: tens of microseconds of overhead.
- Control-plane offline: gateway continues serving from warm cache until JWT TTL expires (15 min default); with bootstrap mode on, it can serve indefinitely.