Control plane API

API keys

Mint, list and revoke keys over the API, with the limits the gateway enforces and the one-time secret.

POST /keys

{
  "project_id": "<uuid>",
  "name": "checkout-summariser-prod",
  "expires_at": "2026-12-31T00:00:00Z",
  "credit_limit_cents": 5000000000,
  "allowed_models": ["IntelligentInference/gpt-oss-120b(Global)"],
  "environment": "production"
}

project_id and name are required; the rest are optional. project_id must be a project in the caller's organisation (404 otherwise, so a session for one organisation cannot mint keys against another's project).

FieldRules
expires_atISO 8601, must be in the future. 400 otherwise.
credit_limit_centsPositive integer, micro-PKR (Rs 5,000 is 5000000000). A lifetime cap for this key.
allowed_modelsNon-empty list of route ids that exist and are active, checked against the catalogue at request time; an unknown id answers 400 naming it.
environmentproduction (default), development or testing. A label only.

Response:

{
  "id": "<uuid>",
  "name": "checkout-summariser-prod",
  "project_id": "<uuid>",
  "key_prefix": "sk_i2_XXXXXXXX",
  "rate_limit_rpm": 60,
  "expires_at": "2026-12-31T00:00:00+00:00",
  "credit_limit_cents": 5000000000,
  "allowed_models": ["IntelligentInference/gpt-oss-120b(Global)"],
  "environment": "production",
  "enforcement_status": {
    "expires_at": "enforced",
    "credit_limit_cents": "enforced",
    "allowed_models": "enforced"
  },
  "api_key": "sk_i2_<32 characters>"
}

Important api_key is the secret and this response is the only time it exists in the clear. It is stored hashed; GET /keys returns key_prefix only.

rate_limit_rpm is not an input: every key is issued at the platform default of 60 and the field is echoed so you can see what applies. enforcement_status states whether the gateway currently enforces each limit; all three are enforced on the request path today, and the field exists so a client never has to assume that from the names alone.

GET /keys

{
  "keys": [
    {
      "id": "<uuid>",
      "name": "checkout-summariser-prod",
      "key_prefix": "sk_i2_XXXXXXXX",
      "project_id": "<uuid>",
      "created_at": "2026-08-04T10:38:49.451247+00:00",
      "last_used_at": null,
      "rate_limit_rpm": 60,
      "expires_at": null,
      "credit_limit_cents": null,
      "allowed_models": null,
      "environment": "production"
    }
  ]
}

Newest first, the caller's organisation only, never a hash or a secret. last_used_at is derived at read time from the request log rather than stored on the key, so null means the key has genuinely never been used. If the analytics store is unreachable when you call this, every row's last_used_at degrades to null for that one response instead of failing the call: retry before concluding a key is unused.

DELETE /keys/{key_id}

No body. Revokes the key; the gateway refuses it on its very next request. 404 for a key that is not the caller's, does not exist, or is already revoked (never a silent 200). Writes an api_key.revoked event to the activity log with the key's prefix as the target. Returns {"message": "API key revoked"}.

PATCH /keys/{key_id}

Changes a key's rate limit, credit limit and expiry. Name, project, model scope and environment are still fixed at creation; to change any of those, mint a new key and revoke the old one.

{
  "rate_limit_rpm": 1000,
  "credit_limit_pkr": 500,
  "expires_at": 1790333428
}

This is a full replace, not a sparse patch. All three fields are evaluated on every call, so send the values you want the key to end up with, not just the ones you are changing.

Two of them deliberately differ from the same concepts on POST /keys, which is worth reading twice before you wire a form to this:

FieldRequiredNotes
rate_limit_rpmYes, every callNo default and no use_platform_max_rate_limit shortcut (that exists only at creation). Above the platform ceiling is a 400.
credit_limit_pkrNoPlain PKR, not the micro-PKR credit_limit_cents that POST /keys takes. Omitted, null or 0 all mean unlimited.
expires_atNoUnix epoch seconds, not the ISO string POST /keys takes. 0, null or omitted mean never expires. Anything not strictly in the future is a 400.

Two invariants are enforced server-side, both 400, and each error names the real figure so a form can show it without a second call:

  1. Spend floor. A key's limit cannot be set below what it has already spent this cycle.
  2. Headroom ceiling. This key's new limit plus every other active key's limit cannot exceed the organisation's budget. A key with no limit of its own contributes nothing to that sum.

Every key in the organisation is locked for the duration of the call, so two edits to different keys fired at the same moment are serialised rather than each reading the other's stale total. You do not need to queue or debounce them client-side.

Returns 200 with the stored values, credit_limit_cents back in micro-PKR:

{
  "id": "<uuid>",
  "key_prefix": "sk_i2_XXXXXXXX",
  "rate_limit_rpm": 1000,
  "expires_at": "2026-09-25T00:00:00+00:00",
  "credit_limit_cents": 500000000
}

404 for a key that is not the caller's. Writes an api_key.updated event to the activity log.

Something unclear or wrong on this page? Tell us. Machine-readable copies: llms.txt.