The management REST API is the same surface the langwatch CLI and LangWatch dashboard use, exposed for scripts, CI pipelines, SDKs, and terraform providers. It runs on the LangWatch control plane (https://app.langwatch.ai), separately from the data-plane gateway (https://gateway.langwatch.ai).
Authentication uses existing LangWatch API tokens (Authorization: Bearer lwp_... or X-Auth-Token: lwp_...). No new token format.
The CLI (langwatch virtual-keys ...) is built on this API. If you’re scripting from Node or Python, consider using the CLI rather than calling the REST directly, it handles pagination, JSON formatting, and error messages for you.
Base
Base URL: https://app.langwatch.ai/api/gateway/v1
Auth: Authorization: Bearer <project_api_token>
Content: application/json
Self-hosted: replace the base with your control plane’s DNS.
Virtual keys
List
Response:
{
"data": [
{
"id": "vk_01HZX...",
"name": "prod-key",
"description": null,
"environment": "live",
"prefix": "vk-lw-01H",
"last_four": "NZ0Z",
"status": "ACTIVE",
"principal_user_id": null,
"project_id": "proj_01HZ...",
"organization_id": "org_01HZ...",
"provider_credential_ids": ["gpc_01HZ...", "gpc_01HZW..."],
"config": { /* model_aliases, cache, fallback, ... */ },
"created_at": "2026-04-10T12:00:00Z",
"updated_at": "2026-04-18T18:21:00Z",
"revoked_at": null,
"last_used_at": "2026-04-18T21:03:12Z"
}
]
}
Create
Body:
{
"name": "ci-key",
"description": "CI smoke tests",
"environment": "live", // or "test"
"principal_user_id": null, // optional
"provider_credential_ids": ["gpc_01HZ..."], // required, min 1
"config": { /* optional partial config */ }
}
Response 201:
{
"virtual_key": { /* full VK shape per List */ },
"secret": "vk-lw-01HZX9K3MABCDEFGH...JIKLMN0Z"
}
The secret field is returned only this once. Persist it immediately.
Get
Response: { "virtual_key": {...} }. No secret.
Update
Body (all fields optional):
{
"name": "new-name",
"description": null, // null clears
"provider_credential_ids": ["gpc_...", "..."],
"config": { /* partial, merges with existing */ }
}
Rotate
POST /virtual-keys/:id/rotate
Response 200:
{
"virtual_key": { /* ... */ },
"secret": "vk-lw-NEW..."
}
The previous secret stops authenticating immediately (modulo ~60 s of gateway L1 cache TTL).
Revoke
POST /virtual-keys/:id/revoke
Idempotent: revoking an already-revoked VK returns 200 with the same status.
Budgets
List
Response: { "data": [{...}, ...] }. Each entry has limit_usd and spent_usd as strings (decimal precision preserved).
Create
Body:
{
"scope": {
"kind": "PROJECT", // or ORGANIZATION, TEAM, VIRTUAL_KEY, PRINCIPAL
"project_id": "proj_..." // the id field depends on scope.kind
},
"name": "project-monthly-cap",
"description": "Standard monthly envelope",
"window": "MONTH", // MINUTE | HOUR | DAY | WEEK | MONTH | TOTAL
"limit_usd": 5000, // or "5000.00"
"on_breach": "BLOCK", // or WARN
"timezone": "Europe/Amsterdam"
}
Update
Updatable fields: name, description, limit_usd, on_breach, timezone.
Archive
Soft archive: preserves ledger history, stops enforcement on new requests.
Provider bindings
List
Response:
{
"data": [
{
"id": "gpc_01HZ...",
"model_provider_id": "mp_openai",
"model_provider_name": "openai",
"slot": "primary",
"rate_limit_rpm": 10000,
"rate_limit_tpm": 1000000,
"rate_limit_rpd": null,
"rotation_policy": "manual",
"fallback_priority_global": 10,
"health_status": "healthy",
"disabled_at": null,
"created_at": "2026-04-10T12:00:00Z"
}
]
}
Create
Body:
{
"model_provider_id": "mp_openai", // the existing provider row id
"slot": "primary", // free-text
"rate_limit_rpm": 10000,
"rate_limit_tpm": 1000000,
"rate_limit_rpd": null,
"rotation_policy": "manual", // v1 accepts "manual" only; auto + external_secret_store are v1.1
"extra_headers": null,
"provider_config": null,
"fallback_priority_global": 10
}
Response 201: { "provider_credential": { "id": "gpc_01HZ..." } }.
Update
Disable
Soft disable: existing VKs bound to this credential continue to resolve; new VK creation rejects this credential.
Cache rules
Organization-scoped overrides that modulate cache behaviour for requests routed through the gateway. Evaluated first-match-wins by priority DESC; a matched rule wins over the per-VK default but loses to a per-request X-LangWatch-Cache header. Full contract in Cache control.
List
Requires gatewayCacheRules:view. Returns rules sorted priority DESC, excluding archived.
{
"data": [
{
"id": "V1StGXR8_Z5jdHi6B-myT",
"organization_id": "org_01HZ...",
"name": "force-cache-enterprise",
"description": "Force cache on Anthropic for enterprise-tagged VKs",
"priority": 300,
"enabled": true,
"matchers": { "vk_tags": ["tier=enterprise"] },
"action": { "mode": "force", "ttl": 600 },
"mode_enum": "FORCE",
"archived_at": null,
"created_at": "2026-04-19T09:00:00Z",
"updated_at": "2026-04-19T09:00:00Z"
}
]
}
mode_enum is echoed in upper case alongside the lower-case action.mode so Prometheus, dashboards can filter by it without parsing the JSON action.
Get
Requires gatewayCacheRules:view. Returns 404 for archived rules, use the audit log to inspect removed rules.
Create
Requires gatewayCacheRules:create. At least one matcher is required (rules that match every request must be declared explicitly, unsupported in v1). Matchers across non-null fields are ANDed.
{
"name": "disable-cache-evals",
"description": "Disable cache for evaluation traffic",
"priority": 200,
"enabled": true,
"matchers": {
"vk_prefix": "vk-lw-",
"request_metadata": { "x-langwatch-suite": "evals" }
},
"action": { "mode": "disable" }
}
Matcher fields (all optional, ANDed):
| field | type | semantics |
|---|
vk_id | string | Exact match against the VK’s display prefix form (vk-lw-...) |
vk_prefix | string | strings.HasPrefix against the same VK display prefix |
vk_tags | string[] | VK must carry EVERY listed tag (AND subset) |
principal_id | string | Exact match against the VK’s principal user |
model | string | Exact match against the resolved model name (no regex; trailing * as a glob is accepted) |
request_metadata | object | Exact-match against request headers/metadata key-by-key |
Action fields:
| field | type | semantics |
|---|
mode | "respect", "force", "disable" | Required. See Cache control for per-provider semantics |
ttl | int seconds | Optional. Clamped to [0, 86400]. Only meaningful for force on providers that support TTL (Anthropic) |
salt | string (max 64) | Optional cache-bust tag, changing this on an existing rule forces regeneration on next hit |
Response 201: { "cache_rule": { ...full row } }.
Update
Requires gatewayCacheRules:update. Partial update; matchers and action replace the stored value when provided (not merged field-by-field). Omitting them leaves the stored value untouched. Name, description, priority, enabled update independently.
Archive
Requires gatewayCacheRules:delete. Soft archive: sets archivedAt. The rule stops matching new requests. Returns the archived row (200, not 204) so scripts can confirm the archivedAt timestamp.
Errors
All responses follow the OpenAI-compatible envelope:
{
"error": {
"type": "bad_request",
"code": "validation_error",
"message": "provider_credential_ids must contain at least 1 element"
}
}
See API: Errors for the full type enum.
Common management-side types:
| HTTP | type | Meaning |
|---|
| 400 | bad_request | Validation, missing required field |
| 401 | unauthenticated | Missing or invalid API token |
| 403 | permission_denied | Token lacks the required RBAC scope |
| 404 | not_found | Resource doesn’t exist or isn’t visible in this project |
| 409 | conflict | Name collision on create, stale update (optimistic concurrency) |
| 422 | validation_error | Semantic error (e.g. window=MINUTE but limit_usd=100000, impossibly large per-minute cap) |
Audit
Every write emits a row in the platform-wide AuditLog (gateway shape, targetKind, targetId, before, after). Visible under /settings/audit-log with the Source = “Gateway” badge; filter by Target (virtual_key, budget, provider_binding, cache_rule) to scope. Writes via API tokens are attributed to the API token’s resolved user. See Audit log for the full schema, REST export path, and migration note from the v3.0 gateway-only table.
Rate limits
Management endpoints are rate-limited to 100 req/min/token. For bulk operations, use the --format json CLI with xargs -P4 (the CLI sleeps 250 ms between retries on 429).
Shared service layer
Both the REST API and the tRPC routers (virtualKeys.*, gatewayBudgets.*, modelProviders.*) call the same service classes on the server, VirtualKeyService, GatewayBudgetService, and the platform-wide ModelProvider service. The only difference between REST and tRPC is the DTO shape (snake_case vs camelCase). Behaviour is identical.
This matters for testing: the BDD scenarios in specs/ai-gateway/public-rest-api.feature include a parity check that asserts the two surfaces return equivalent data for the same resource after case normalization. If you rely on a field in one, it’s guaranteed to exist in the other.
OpenAPI
All REST routes are annotated with hono-openapi’s describeRoute schemas. The generated OpenAPI 3.1 spec is served at /api/gateway/v1/openapi.json and renders inside the platform’s API-reference UI. The CLI’s VirtualKeysApiService, GatewayBudgetsApiService DTOs are hand-authored against the same schemas; a v1.1 improvement is to regenerate them from the OpenAPI output so drift fails at build time.
See also
- langwatch CLI: higher-level access.
- RBAC: which scopes your token needs.
- Security: how your API tokens and resulting writes are protected.