API keys

Create, list, rotate, disable, and delete a project's inference API keys.

Project API keys are the credentials your applications use on the Inference API. This page manages their whole lifecycle — provision · configure · rotate · retire — with the same semantics as the portal, and every write is recorded in the audit log with the token that made it.

Keys are created under a project and addressed flat by id afterwards.

Unlike project, member and organization writes, key routes are not Admin-only. They follow the portal's rules for the token owner: an Admin manages every key; a Member sees and manages only the keys they created, and creates keys only in projects they belong to. Anything else is 403 insufficient_scope (the message says which rule) or 404.

The Python examples share this setup:

import os
import uuid
import requests

BASE = "https://portal.<domain>/api/v1/mgmt"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['TF_PAT']}"

def idem():
    return {"Idempotency-Key": str(uuid.uuid4())}

The key object

FieldTypeNotes
idstringOpaque id.
project_idstringThe owning project.
namestring
descriptionstring | null
key_prefixstringMasked form for display, e.g. tf-4b8a9c-7d21e0-Qm8...k2Zw. Never the secret.
statusstringactive, disabled or revoked.
created_atstringISO-8601 UTC.
last_used_atstring | null
expires_atstring | nullnull = no expiry.
revoked_atstring | nullSet once the key is deleted.
requests_7dintegerRequests in the last 7 days.
allowed_modelsstring[]Models this key may call (provider/model); empty = the project's allowlist applies.
rpm_limitinteger | nullThe key's own requests-per-minute limit.
spend_capobject | nullLegacy, read-only: a key-level spend cap stored before spend limits moved to the project — { "amount_usd", "reset_period" }, where reset_period is daily, weekly, monthly or never. null = no key cap. It cannot be set or changed through this API; set spend limits on the project.
ip_allowliststring[]CIDR ranges; empty = the project's allowlist applies.
rotationobject | nullPresent while a rotation is in progress: { "grace_expires_at": "…" } — the previous secret keeps working until then.
created_byobject | null{ "id", "email" } of the creator. Immutable history — it persists after the creator leaves, and nothing reassigns it.
{
  "id": "c41d8e2a-7b90-4f3c-a6e5-2d1f0b9c8a74",
  "project_id": "5b3e9a41-0c7d-4e2f-8a16-9d4b7c2e1f08",
  "name": "payments-prod",
  "description": null,
  "key_prefix": "tf-4b8a9c-7d21e0-Qm8...k2Zw",
  "status": "active",
  "created_at": "2026-09-20T08:20:44Z",
  "last_used_at": "2026-09-24T09:58:12Z",
  "expires_at": "2027-03-31T00:00:00Z",
  "revoked_at": null,
  "requests_7d": 48213,
  "allowed_models": [],
  "rpm_limit": null,
  "spend_cap": null,
  "ip_allowlist": [],
  "rotation": null,
  "created_by": { "id": "8e7d6c5b-4a39-4281-9f0e-1d2c3b4a5968", "email": "[email protected]" }
}

List a project's keys

GET /api/v1/mgmt/projects/{id}/keys

Returns the project's keys (masked), newest first — including revoked keys, for the record. A Member sees only the keys they created. Paginated with limit and cursor.

Query parameterNotes
statusactive, disabled or revoked. Any other value → 422 validation_failed.
curl -G https://portal.<domain>/api/v1/mgmt/projects/5b3e9a41-0c7d-4e2f-8a16-9d4b7c2e1f08/keys \
  -H "Authorization: Bearer $TF_PAT" \
  -d status=active

Create a key

POST /api/v1/mgmt/projects/{id}/keys
Body fieldTypeRequiredNotes
namestringyesUnique among your active keys (409 name_taken).
descriptionstringno
allowed_modelsstring[]noprovider/model ids. An unknown model → 400, param: "allowed_models".
rpm_limitintegernoAt least 1.
expires_atstringnoRFC 3339 timestamp.
ip_allowliststring[]noCIDR notation.

Requires Idempotency-Key. Returns 201 with the key object plus secret — the full key value.

Spend limits are set on the project (spend_limit_usd), not on keys. A body carrying spend_cap_amount, spend_cap_reset_period or clear_spend_cap — any value, including null — is rejected with 400 invalid_request, param naming the field.

Warning

The secret is returned once, in this response only. Every later read returns the masked key_prefix. If the response is lost, retry with the same Idempotency-Key within 24 hours (the replay returns the same secret and is audited as key_create_replayed) or rotate the key. Write the secret straight to your secret store; log the key id, never the value.

curl -X POST https://portal.<domain>/api/v1/mgmt/projects/5b3e9a41-0c7d-4e2f-8a16-9d4b7c2e1f08/keys \
  -H "Authorization: Bearer $TF_PAT" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: run-48213-create-key" \
  -d '{"name": "payments-prod", "expires_at": "2027-03-31T00:00:00Z"}'
{
  "id": "c41d8e2a-7b90-4f3c-a6e5-2d1f0b9c8a74",
  "project_id": "5b3e9a41-0c7d-4e2f-8a16-9d4b7c2e1f08",
  "name": "payments-prod",
  "description": null,
  "key_prefix": "tf-4b8a9c-7d21e0-Qm8...k2Zw",
  "status": "active",
  "created_at": "2026-09-24T10:02:40Z",
  "last_used_at": null,
  "expires_at": "2027-03-31T00:00:00Z",
  "revoked_at": null,
  "requests_7d": 0,
  "allowed_models": [],
  "rpm_limit": null,
  "spend_cap": null,
  "ip_allowlist": [],
  "rotation": null,
  "created_by": { "id": "8e7d6c5b-4a39-4281-9f0e-1d2c3b4a5968", "email": "[email protected]" },
  "secret": "tf-4b8a9c-7d21e0-Qm8xR2v…(full value, shown once)"
}

Retrieve a key

GET /api/v1/mgmt/keys/{id}

Returns the key object (masked).

curl https://portal.<domain>/api/v1/mgmt/keys/c41d8e2a-7b90-4f3c-a6e5-2d1f0b9c8a74 \
  -H "Authorization: Bearer $TF_PAT"

Update a key

PATCH /api/v1/mgmt/keys/{id}

Changes configuration only. Send at least one field; only the fields you send change. Returns 200 with the key object. Key-level spend-cap fields are rejected exactly as on create — set spend limits on the project.

Body fieldTypeNotes
namestring
expires_atstring | nullRFC 3339 timestamp; null removes the expiry.
ip_allowliststring[] | nullCIDR notation; [] or null = no key-level restriction.

State has its own verbs (disable, enable, delete) — PATCH never changes state.

curl -X PATCH https://portal.<domain>/api/v1/mgmt/keys/c41d8e2a-7b90-4f3c-a6e5-2d1f0b9c8a74 \
  -H "Authorization: Bearer $TF_PAT" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f" \
  -d '{"name": "payments-prod-eu", "ip_allowlist": ["198.51.100.0/24"]}'

Rotate a key

POST /api/v1/mgmt/keys/{id}/rotate

Issues a new secret for the same key id. The previous secret keeps working for a grace window you choose, so you can deploy the new one without downtime.

Body fieldTypeRequiredNotes
grace_hoursintegernoHow long the previous secret keeps working: 1–168 (7 days). Default 24. The body may be omitted.

Returns 200 with the key object plus secret (the new value, once). The old secret stops working at rotation.grace_expires_at. A replay with the same Idempotency-Key returns the same secret and is audited as key_rotate_replayed.

curl -X POST https://portal.<domain>/api/v1/mgmt/keys/c41d8e2a-7b90-4f3c-a6e5-2d1f0b9c8a74/rotate \
  -H "Authorization: Bearer $TF_PAT" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: run-48213-rotate-key" \
  -d '{"grace_hours": 2}'
{
  "id": "c41d8e2a-7b90-4f3c-a6e5-2d1f0b9c8a74",
  "project_id": "5b3e9a41-0c7d-4e2f-8a16-9d4b7c2e1f08",
  "name": "payments-prod",
  "key_prefix": "tf-4b8a9c-7d21e0-Zt4...9pQa",
  "status": "active",
  "rotation": { "grace_expires_at": "2026-09-24T12:05:00Z" },
  "secret": "tf-4b8a9c-7d21e0-Zt4mW8e…(full value, shown once)",
  "...": "..."
}

Warning

If the grace window ends before the new secret is deployed, calls with the old secret fail with 401. This is the most common self-inflicted incident in API-driven key management — size grace_hours to your rollout and watch for keys stuck mid-rotation.

Cancel a rotation

POST /api/v1/mgmt/keys/{id}/rotate/cancel

Aborts an in-progress rotation: the new secret is discarded and the original secret stays active with no deadline. This is the clean abort for a failed deploy. No body. Returns 200 with the key object (rotation: null). A key with no rotation in progress is 409 no_rotation_in_progress.

curl -X POST https://portal.<domain>/api/v1/mgmt/keys/c41d8e2a-7b90-4f3c-a6e5-2d1f0b9c8a74/rotate/cancel \
  -H "Authorization: Bearer $TF_PAT" \
  -H "Idempotency-Key: run-48213-cancel-rotation"

Disable a key

POST /api/v1/mgmt/keys/{id}/disable

Reversible: the key stops authenticating inference until you enable it. No body. Returns 200 with the key object (status: "disabled").

curl -X POST https://portal.<domain>/api/v1/mgmt/keys/c41d8e2a-7b90-4f3c-a6e5-2d1f0b9c8a74/disable \
  -H "Authorization: Bearer $TF_PAT" \
  -H "Idempotency-Key: 3d4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f6a"

Enable a key

POST /api/v1/mgmt/keys/{id}/enable

The inverse of disable. No body. Returns 200 with the key object (status: "active"). A deleted (revoked) key cannot come back: 409 key_revoked.

curl -X POST https://portal.<domain>/api/v1/mgmt/keys/c41d8e2a-7b90-4f3c-a6e5-2d1f0b9c8a74/enable \
  -H "Authorization: Bearer $TF_PAT" \
  -H "Idempotency-Key: 4e5f6a7b-8c9d-4e0f-9a1b-2c3d4e5f6a7b"

Delete a key

DELETE /api/v1/mgmt/keys/{id}

The permanent kill. The key stops authenticating and cannot be restored; it stays readable with status: "revoked" and a revoked_at, and its usage history and audit trail are retained. A compromised secret gets no grace window — use this for a confirmed leak. Returns 204 No Content.

Body fieldTypeRequiredNotes
confirmed_leakbooleannotrue also cuts off requests already streaming on the key. Default false. The body may be omitted.
curl -X DELETE https://portal.<domain>/api/v1/mgmt/keys/c41d8e2a-7b90-4f3c-a6e5-2d1f0b9c8a74 \
  -H "Authorization: Bearer $TF_PAT" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: incident-4711-delete-key" \
  -d '{"confirmed_leak": true}'

Note

To retire every key a departing member created, either remove the member with DELETE /members/{id} and "delete_keys": true, or list the project's keys, filter on created_by.id, and delete or rotate each by id.

Errors specific to keys

StatusCodeWhen
400invalid_request (param set)Missing name; unknown field; grace_hours outside 1–168; bad expires_at; bad CIDR; a key-level spend-cap field (spend_cap_amount, spend_cap_reset_period, clear_spend_cap — spend limits are set on the project); an unknown model in allowed_models.
403insufficient_scopeToken lacks keys:read/keys:write; a Member owner acting on a key they did not create, or in a project they do not belong to.
404not_foundKey or project absent, or in another organization.
409name_takenYou already have an active key with this name.
409key_revokedEnabling a deleted key.
409no_rotation_in_progressCancelling when no rotation is in progress.
409idempotency_conflictThe Idempotency-Key was used for a different request.
422unsupported_filter · validation_failedAn unknown query parameter, or a status filter value outside the list above.