---
title: "Update an API key"
method: PATCH
path: "/v2/api-keys/{api_key_id}"
tags: ["API keys"]
---

# Update an API key

`PATCH /v2/api-keys/{api_key_id}`

Updates mutable fields of an API key: display name, status (active / disabled / revoked), permission mode and access map, project scope, and constraints (budget / rate limit / expiry). Omitted fields keep their current values.

## Path parameters

- `api_key_id` string, required

## Request body

- UpdateApiKeyRequest
  - `name` string — New name. Omit to keep current.
  - `status` 'API_KEY_STATUS_UNSPECIFIED' | 'API_KEY_STATUS_ACTIVE' | 'API_KEY_STATUS_DISABLED' | 'API_KEY_STATUS_REVOKED'
  - `permission_mode` 'PERMISSION_MODE_UNSPECIFIED' | 'PERMISSION_MODE_ALL' | 'PERMISSION_MODE_RESTRICTED' | 'PERMISSION_MODE_READ_ONLY'
  - `access` object — Replacement access map. Required when changing to `PERMISSION_MODE_RESTRICTED`; ignored otherwise. Provide an empty map to clear. See `ApiKey.access` for the full catalog of valid keys (Domain.id) and AccessLevel string values, or fetch the live catalog via the capability catalog endpoint.
  - `project_scope` ProjectScope — Project authorization scope. Single-project or all-projects. Multi-project use cases are served by minting per-project keys or by using an all-projects key with `restricted` mode.
    - `all` AllProjects
    - `single` SingleProject
      - `project_id` string, required — Project ID this API key is scoped to.
  - `expires_at` string, date-time — New expiration. Omit to keep current. Set `clear_expires_at = true` to remove an existing expiration (a zero Timestamp here would still mean "no change" because of optional semantics).
  - `clear_expires_at` boolean — Force-clear the expiration. Mutually exclusive with `expires_at`.

## Response `200`

OK

- UpdateApiKeyResponse
  - `api_key` ApiKey, required — ApiKey is the canonical record stored in MongoDB `auth.apiKeys`. It is the source of truth for scope, permissions, owner, budget, expiration, and revocation (see ADR 0001).
    - `api_key_id` string, required — Canonical key identifier (ULID). Embedded in opaque tokens as `sk-orq-<id>-<secret>`.
    - `name` string, required — Human-readable name shown in the dashboard.
    - `owner` ApiKeyOwner, required — Owner attribution drives lifecycle. `service_account` keys are workspace-owned and outlive any individual user. `user` keys are bound to `user_id`: when the user is removed, disabled, or loses project access, the key is revoked / its scope shrinks per the cascade rules in ADR 0001.
      - `user` UserOwner
        - `user_id` string, required — User ID that owns the API key.
      - `service_account` ServiceAccountOwner
    - `project_scope` ProjectScope, required — Project authorization scope. Single-project or all-projects. Multi-project use cases are served by minting per-project keys or by using an all-projects key with `restricted` mode.
      - `all` AllProjects
      - `single` SingleProject
        - `project_id` string, required — Project ID this API key is scoped to.
    - `permission_mode` 'PERMISSION_MODE_UNSPECIFIED' | 'PERMISSION_MODE_ALL' | 'PERMISSION_MODE_RESTRICTED' | 'PERMISSION_MODE_READ_ONLY', required
    - `access` object — Per-domain access map. Only populated when `permission_mode` is `PERMISSION_MODE_RESTRICTED`. The authoritative list of valid keys and the per-domain read / write semantics are exposed at runtime via the capability catalog endpoint. Valid keys are the Domain.id values in the capability catalog — see libs/catalog/orq/apikeys/v1/catalog.textpb for the canonical list. The ids are intentionally not duplicated here to avoid drift. Values are AccessLevel enum names on the JSON wire: "ACCESS_LEVEL_NONE" — capability not granted "ACCESS_LEVEL_READ" — list / view verbs "ACCESS_LEVEL_WRITE" — list / view + mutating / execute verbs
    - `token_prefix` string, required — Displayable prefix for UI listings (e.g. "sk-orq-01HXY..."). Safe to expose.
    - `status` 'API_KEY_STATUS_UNSPECIFIED' | 'API_KEY_STATUS_ACTIVE' | 'API_KEY_STATUS_DISABLED' | 'API_KEY_STATUS_REVOKED', required
    - `created_by_id` string — Audit: user who created the key. Optional. Distinct from `owner.user_id` — created_by is provenance only, while owner determines lifecycle binding.
    - `updated_by_id` string — Audit: user who last updated the key.
    - `created_at` string, date-time, required — Time the key was created.
    - `updated_at` string, date-time, required — Time the key was last updated.
    - `last_used_at` string, date-time — Last authenticated use. Updated via NATS debounce + 1% sampler.
    - `expires_at` string, date-time — Optional expiration. The authenticate hot-path rejects keys whose `expires_at` is in the past. Unset means the key never expires.
    - `legacy_token_family` 'LEGACY_TOKEN_FAMILY_UNSPECIFIED' | 'LEGACY_TOKEN_FAMILY_ROUTER_JWT' | 'LEGACY_TOKEN_FAMILY_PROJECT_JWT' | 'LEGACY_TOKEN_FAMILY_WORKSPACE_JWT'
    - `legacy_key_id` string — Legacy MongoDB `_id` from before the canonical ULID was assigned. Used by the adapter to resolve a JWT back to this canonical record.
    - `budget` Budget — Budget is the canonical record stored in MongoDB `budgets.entities`. It replaces the embedded `constraints.budget` on api-keys and the legacy CONTACT-only `budgets.configs` collection (see ADR 0007).
      - `budget_id` string
      - `scope` BudgetScope — BudgetScope is a closed oneof. Exactly one variant must be set. The six variants are ordered by enforcement precedence (most specific to most general) and mirror the BudgetScopeKind filter enum.
        - `workspace` WorkspaceBudgetScope — Workspace-wide ceiling. The implicit target is the caller's workspace.
        - `project` ProjectBudgetScope — Per-project cap.
          - `project_id` string
        - `identity` IdentityBudgetScope — Per-identity cap. Keyed by the contact's external_id (not the internal Mongo `_id`) so the scope is stable across imports.
          - `identity_external_id` string
        - `api_key` ApiKeyBudgetScope — Per-api-key cap. Replaces the legacy embedded `constraints.budget` on auth.apiKeys.
          - `api_key_id` string
        - `provider` ProviderBudgetScope — Per-provider cap. The value is the provider enum string (e.g. "openai", "anthropic") drawn from ModelIntegrationIdentifier.
          - `provider` string
        - `model` ModelBudgetScope — Per-model cap. The value is the FULL model reference as callers send it ("openai/gpt-4o", or "workspaceKey@openai/gpt-4o" for private models) — NOT the Mongo `_id` of the model master-data document.
          - `model_id` string
      - `match` BudgetMatch — BudgetMatch carries the CEL expression that decides whether a budget applies to a request. Available variables: `model`, `provider`, `model_id`, `api_key`, `identity`, `project`, `metadata` (map), `headers` (map, lowercase keys). An empty expression always matches. Expressions are syntax-validated at write time.
        - `cel` string
      - `limits` BudgetLimits — BudgetLimits is the per-period spend and token ceiling. At least one of `amount`, `token_limit`, or RateLimit.requests_per_minute MUST be set on a Budget; that invariant is enforced by the handler.
        - `period` 'BUDGET_PERIOD_UNSPECIFIED' | 'BUDGET_PERIOD_DAILY' | 'BUDGET_PERIOD_WEEKLY' | 'BUDGET_PERIOD_MONTHLY' | 'BUDGET_PERIOD_YEARLY' | 'BUDGET_PERIOD_ONE_TIME'
        - `amount` number, double
        - `token_limit` number, double — Token ceiling. Carried as a double so it serializes as a JSON number (proto int64 would serialize as a quoted string); token counts are whole and well within double's exact-integer range (2^53). Stored as an integer server-side.
      - `rate_limit` RateLimit — RateLimit is the per-minute request ceiling. Enforced via atomic increment-first semantics in the enforcement middleware.
        - `requests_per_minute` integer
      - `is_active` boolean
      - `expires_at` string, date-time
      - `created_at` string, date-time
      - `updated_at` string, date-time
      - `usage` BudgetUsage — BudgetUsage is the current-period consumption of a budget, sourced from the live Redis counters (not the exact ledger). Each dimension is the consumed side of the matching limit dimension: `amount` is the accumulated cost in USD (vs limits.amount), `tokens` is the accumulated token count (vs limits.token_limit), and `requests` is the count in the rolling 60-second window (vs rate_limit.requests_per_minute). All three are explicit-presence so the triple is always emitted in full, zeros included — a never-spent budget serializes {amount:0, tokens:0, requests:0} rather than dropping its zero dimensions.
        - `amount` number, double
        - `tokens` number, double — Carried as a double (not int64) so it serializes as a JSON number rather than a quoted string, matching limits.token_limit.
        - `requests` integer

## Changes

- **2026-06-30** `877151607ea9` — 1 info
  - added the optional property `api_key/allOf[#/components/schemas/ApiKey]/budget` to the response with the `200` status

[Change history](https://skmtc.dev/orq-ai/apis/orq-ai-api/changes/v2/api-keys/:api_key_id/patch.md)

---

[API](https://skmtc.dev/orq-ai/apis/orq-ai-api.md) · [All operations](https://skmtc.dev/orq-ai/apis/orq-ai-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/orq-ai/orq-ai-api/revisions/320d42e024f2/schema)
