> ## Documentation Index
> Fetch the complete documentation index at: https://langwatch.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Management REST API

> Manage virtual keys, budgets, and routing policies programmatically via /api/gateway/v1/*.

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.

<Info>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.</Info>

## 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

```
GET /virtual-keys
```

Returns every key visible to your credential: keys scoped to your project, its team, or the whole organization.

Response:

```json theme={null}
{
  "data": [
    {
      "id": "vk_01HZX...",
      "organization_id": "org_01HZ...",
      "name": "prod-key",
      "description": null,
      "status": "active",
      "purpose": "user",
      "display_prefix": "vk-lw-01HZX9",
      "principal_user_id": null,
      "trace_project_id": null,
      "scopes": [{ "scope_type": "PROJECT", "scope_id": "proj_01HZ..." }],
      "routing_policy_id": null,
      "routing_mode": "NONE",
      "config": { /* modelsAllowed, model_aliases, cache, fallback, ... */ },
      "revision": "3",
      "created_at": "2026-04-10T12:00:00Z",
      "updated_at": "2026-04-18T18:21:00Z",
      "last_used_at": "2026-04-18T21:03:12Z",
      "revoked_at": null
    }
  ]
}
```

`purpose: "langy"` marks a product-managed key (auto-provisioned by LangWatch). Those rows appear in listings but refuse every mutation; filter on `purpose` when scripting bulk operations.

### Create

```
POST /virtual-keys
```

Body (only `name` is required):

```json theme={null}
{
  "name": "ci-key",
  "description": "CI smoke tests",
  "principal_user_id": null,
  "scopes": [{ "scope_type": "PROJECT", "scope_id": "proj_01HZ..." }],
  "routing_mode": "NONE",
  "routing_policy_id": null,
  "budget": { "limit_usd": "25", "window": "MONTH", "on_breach": "BLOCK" },
  "config": { "providersAllowed": ["mp_openai..."] }
}
```

* `scopes` defaults to the calling project. Team- and org-scoped keys need a scoped API key holding `virtualKeys:manage` at each requested scope; legacy project keys can only mint keys for their own project.
* An org- or team-scoped key also needs a place for its traces and spend to land: pass `trace_project_id` (requires `virtualKeys:manage` on that project; it is a destination, not a scope, and grants no access to the key), or the organization's governance project is used, and creation refuses with `trace_project_required` when neither exists.
* `budget` creates a budget atomically with the key and manages exactly that row on later updates: the key can never exist without the cap you asked for. Windows: `DAY`, `WEEK`, `MONTH`.
* `routing_mode` is one of `NONE` (default: no silent failover), `FALLBACK_ALL`, or `POLICY` (requires `routing_policy_id`).

Response `201`:

```json theme={null}
{
  "virtual_key": { /* full VK shape per List */ },
  "secret": "vk-lw-01HZX9K3MABCDEFGH...JIKLMN0Z"
}
```

<Warning>The `secret` field is returned **only this once**. Persist it immediately.</Warning>

### Get

```
GET /virtual-keys/:id
```

Response: `{ "virtual_key": {...} }`. No secret.

### Spend

```
GET /virtual-keys/:id/spend?from=2026-07-01T00:00:00Z&to=2026-07-15T00:00:00Z
```

Requires `gatewayUsage:view`. Both window params are optional; the default window is the current UTC calendar month. Reads the same cost path (`trace_summaries`) the dashboard reads, so this number and the UI agree by construction.

Response:

```json theme={null}
{
  "virtual_key_id": "vk_01HZX...",
  "spent_usd": "12.4531",
  "requests": 1841,
  "window": { "from": "2026-07-01T00:00:00.000Z", "to": "2026-07-15T00:00:00.000Z" }
}
```

On deployments without a ClickHouse spend source the endpoint answers `412` with `error.code = "spend_source_unavailable"` rather than a `$0.00` that cannot be told apart from a zero-spend key.

### Update

```
PATCH /virtual-keys/:id
```

Body (all fields optional):

```jsonc theme={null}
{
  "name": "new-name",
  "scopes": [{ "scope_type": "TEAM", "scope_id": "team_..." }],
  "routing_mode": "FALLBACK_ALL",
  "budget": { "limit_usd": "50", "window": "MONTH" },
  "config": { /* partial, merges with existing */ }
}
```

`scopes` replaces the whole visibility set and requires `virtualKeys:manage` at every NEW scope. `budget: null` archives the key's own cap (spend history is retained); omitting `budget` leaves it alone.

### Rotate

```
POST /virtual-keys/:id/rotate
```

Response `200`:

```jsonc theme={null}
{
  "virtual_key": { /* ... */ },
  "secret": "vk-lw-NEW..."
}
```

The previous secret keeps authenticating for a 24 h grace window so in-flight clients can roll over.

### Revoke

```
POST /virtual-keys/:id/revoke
```

Idempotent: revoking an already-revoked VK returns `200` with the same status. Revocation also archives the key's own budgets.

## Budgets

### List

```
GET /budgets
GET /budgets?scope_type=VIRTUAL_KEY,PRINCIPAL
```

Returns every non-archived budget in your organization across all six scope types (`ORGANIZATION`, `TEAM`, `PROJECT`, `VIRTUAL_KEY`, `PRINCIPAL`, `GROUP`), with live `spent_usd` from the spend ledger. `scope_type` is an optional comma-separated filter.

Response:

```json theme={null}
{
  "spend_available": true,
  "data": [
    {
      "id": "bgt_01HZ...",
      "organization_id": "org_01HZ...",
      "scope_type": "VIRTUAL_KEY",
      "scope_id": "vk_01HZX...",
      "name": "customer-acme cap",
      "description": null,
      "window": "MONTH",
      "on_breach": "BLOCK",
      "limit_usd": "25",
      "spent_usd": "12.4531",
      "timezone": null,
      "provider_key": null,
      "current_period_started_at": "2026-07-01T00:00:00.000Z",
      "resets_at": "2026-08-01T00:00:00.000Z",
      "last_reset_at": null,
      "archived_at": null,
      "created_at": "2026-06-12T10:00:00.000Z"
    }
  ]
}
```

* `spend_available: false` means spend could not be totalled server-side; do not read `spent_usd` as real spend in that case.
* `GROUP` rows are per-member allowances: `limit_usd` is what EACH member may spend, `spent_usd` is the group's summed spend, and `member_count` says how many members the allowance currently covers.
* `provider_key` names the ModelProvider the budget is pinned to; `null` counts every provider.

### Create

```
POST /budgets
```

Body:

```jsonc theme={null}
{
  "scope": {
    "kind": "PROJECT",         // or ORGANIZATION, TEAM, VIRTUAL_KEY, PRINCIPAL, GROUP
    "project_id": "proj_..."   // the id field depends on scope.kind:
                               // organization_id | team_id | project_id |
                               // virtual_key_id | principal_user_id | group_id
  },
  "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",
  "provider_key": null         // optional ModelProvider id to pin the budget to one provider
}
```

`GROUP` budgets track spend per member, which requires the ClickHouse spend ledger; deployments without it answer `400` with `error.code = "group_budget_requires_clickhouse"`.

### Update

```
PATCH /budgets/:id
```

Updatable fields: `name`, `description`, `limit_usd`, `on_breach`, `timezone`. Scope and window are immutable after create.

### Archive

```
DELETE /budgets/:id
```

Soft archive: preserves ledger history, stops enforcement on new requests. Returns the archived row with `archived_at` set.

## Provider bindings

The `/providers` routes are **gone** (`410`). Gateway provider bindings were folded into the platform-wide ModelProvider in iteration 110: configure credentials, rate limits, and fallback priority via `/api/gateway-platform/v1/model-providers` or the Advanced (Gateway) tab in the dashboard. Budgets reference providers by ModelProvider id (`provider_key`).

## 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](/docs/ai-gateway/cache-control).

### List

```
GET /cache-rules
```

Requires `gatewayCacheRules:view`. Returns rules sorted priority DESC, excluding archived.

```json theme={null}
{
  "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

```
GET /cache-rules/:id
```

Requires `gatewayCacheRules:view`. Returns 404 for archived rules, use the audit log to inspect removed rules.

### Create

```
POST /cache-rules
```

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.

```json theme={null}
{
  "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](/docs/ai-gateway/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

```
PATCH /cache-rules/:id
```

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

```
DELETE /cache-rules/:id
```

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:

```json theme={null}
{
  "error": {
    "type": "bad_request",
    "code": "trace_project_required",
    "message": "trace_project_required: an organization- or team-owned key needs a project for its traces and costs to land in..."
  }
}
```

`error.code` carries the machine code your integration can branch on (`validation_error`, `trace_project_required`, `routing_policy_required`, `scope_org_mismatch`, `group_budget_requires_clickhouse`, `spend_source_unavailable`, ...). See [API: Errors](/docs/ai-gateway/api/errors) for the full type enum.

Common management-side types:

| HTTP | `type`                | Meaning                                                               |
| ---- | --------------------- | --------------------------------------------------------------------- |
| 400  | `bad_request`         | Validation, missing required field, cross-org scope                   |
| 401  | `unauthenticated`     | Missing or invalid API token                                          |
| 403  | `permission_denied`   | Token lacks the required RBAC permission (route ceiling or per-scope) |
| 404  | `not_found`           | Resource doesn't exist or isn't visible to this credential            |
| 410  | `gone`                | Route folded away (the `/providers` bindings)                         |
| 412  | `precondition_failed` | Deployment lacks a required capability (`spend_source_unavailable`)   |

## 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`, `cache_rule`) to scope. Writes via scoped API keys are attributed to the key's owning user; legacy project keys carry no user, so their writes record the machine principal `svc_<projectId>`. See [Audit log](/docs/ai-gateway/audit) 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.*`, `gatewayUsage.*`) call the same service classes and pre-flight asserts on the server: `VirtualKeyService`, `GatewayBudgetService`, `GatewayUsageService`, and the scope-authorization helpers in `virtualKey.authz`. The only difference between REST and tRPC is the DTO shape (snake\_case vs camelCase) and how the caller is identified (API credential vs browser session). Behaviour is identical, and the integration suite in `langwatch/src/app/api/gateway-platform/__tests__/` pins the refusals (`trace_project_required`, per-scope `permission_denied`, `group_budget_requires_clickhouse`, ...) so the two surfaces cannot drift apart again.

## OpenAPI

All REST routes are annotated with `hono-openapi`'s `describeRoute` schemas, which generate into the repository-wide OpenAPI 3.1 document (`docs/api-reference/openapiLangWatch.json`) rendered by the API-reference docs. The CLI's `VirtualKeysApiService` and `GatewayBudgetsApiService` DTOs are hand-authored against the same schemas; a future improvement is to regenerate them from the OpenAPI output so drift fails at build time.

## See also

* [langwatch CLI](/docs/ai-gateway/cli/langwatch-cli): higher-level access.
* [RBAC](/docs/ai-gateway/rbac): which scopes your token needs.
* [Security](/docs/ai-gateway/security): how your API tokens and resulting writes are protected.
