API keys

A Catentio API key is a long-lived bearer credential that lets a server-side caller authenticate to the catentio runtime (not the control plane — see the callout below) as a specific workspace. Keys are minted in the portal, scoped to one customer_id, and presented on requests as:

Authorization: Bearer cat_pk_5fGZ8h2KqWnpL3vBxYsR6tAjMdC1eIuN

Use a key when:

  • The caller isn't you — it's your backend, a cron job, an external integration.
  • You want a credential whose lifetime you control (rotation, revocation) independent of any Huudis session.
  • You want zero round-trips to Huudis on every call.

If the caller is you, use the OIDC device flow instead — an API key isn't tied to a Huudis identity and bypasses MFA.

The control plane doesn't check API keys — the runtime does. The /v1/api-keys endpoints on this page (list/create/revoke) are portal-session routes: they mint and manage the cat_pk_* secret, but the secret itself is verified by the catentio runtime's auth guard on requests you send it directly, not by anything on the control plane. There is only one real prefix, cat_pk_*; don't rely on a cat_ak_* prefix appearing anywhere — it isn't part of the shipped auth model.

The API key object

{
  "id": 42,
  "customer_id": "internal",
  "name": "production-cron",
  "key_prefix": "cat_pk_5fGZ8h2K",
  "created_at": "2026-05-01T08:00:00.000Z",
  "last_used_at": "2026-05-12T10:42:00.123Z",
  "revoked_at": null
}
Field Type Notes
id int Auto-incrementing integer; the PK used in delete URLs.
customer_id string Owning workspace. Force-set server-side from the session.
name string Human-readable label. Shown in the portal listing.
key_prefix string First 12 characters of the secret, for at-a-glance identification.
created_at timestamp ISO 8601 UTC.
last_used_at timestamp | null Updated on every request; rounded to the minute.
revoked_at timestamp | null When deleted; soft-deleted rows stay queryable for audit.

The secret (cat_pk_*) is only returned once, on the create response. After that the API only ever shows key_prefix — the full secret lives in your secret manager, never on the control plane in plaintext.

scopes is a placeholder. The create body accepts a scopes field but the control plane strips it before forwarding (payload.pop("scopes", None)) — v1 keys always carry an empty scopes object, which means full access to the customer's data. There's no scope value worth setting yet; per-resource scoping is a later release, same as the object table below already says.

Endpoints

All endpoints scope to the caller's customer_id automatically. There's no way to list, mint, or revoke another workspace's keys even with a forged URL.

List API keys

GET /v1/api-keys

Returns every key for the caller's workspace — revoked ones included, sorted with active keys first (then newest-first). Check revoked_at on each row to tell them apart; there's no ?status= filter to ask for active-only.

const { data } = await catentio.apiKeys.list();
keys = catentio.apiKeys.list()
curl 'https://catent.io/api/v1/cp/v1/api-keys' \
  -H "Cookie: catentio_session=<session cookie set at login>"

Create an API key

POST /v1/api-keys

Mint a new PAT. Body:

{ "name": "production-cron" }

The customer_id is force-set from the verified session; you can't pass it. Likewise scopes is stripped in v1 — the only available scope is full workspace access. Per-resource scopes land in a later release.

Response (the only time the secret is shown):

{
  "id": 42,
  "customer_id": "internal",
  "name": "production-cron",
  "key": "cat_pk_5fGZ8h2KqWnpL3vBxYsR6tAjMdC1eIuN",
  "key_prefix": "cat_pk_5fGZ8h2K",
  "created_at": "2026-05-01T08:00:00.000Z"
}
const { key } = await catentio.apiKeys.create({ name: 'production-cron' });
console.log('Store this NOW:', key);
result = catentio.apiKeys.create({"name": "production-cron"})
print("Store this NOW:", result["key"])
curl -X POST 'https://catent.io/api/v1/cp/v1/api-keys' \
  -H "Cookie: catentio_session=<session cookie set at login>" \
  -H "Content-Type: application/json" \
  -d '{"name":"production-cron"}'

The secret is shown once. Copy it into your secret manager immediately. The control plane stores only a sha256 hash; there is no recovery flow. If you lose the secret you must mint a new key and revoke the old one.

Revoke an API key

DELETE /v1/api-keys/{id}

Soft-deletes a key (sets revoked_at; the row stays for audit). The CP verifies the key belongs to the caller's customer_id before forwarding — cross-workspace deletes return 403 not_your_key.

Returns 204 No Content. After revocation, any request signed with the key gets a plain-text 401 from the runtime ("api key revoked").

await catentio.apiKeys.delete(42);
catentio.apiKeys.delete(42)
curl -X DELETE 'https://catent.io/api/v1/cp/v1/api-keys/42' \
  -H "Cookie: catentio_session=<session cookie set at login>"

Rotation

Keys don't expire on their own — the v1 control plane has no max-age policy. Rotate them yourself:

  1. Mint a new key with the next name (e.g., production-cron-2026q3).
  2. Deploy it: update your secret manager + restart any callers that cache the key.
  3. Smoke test with the new key.
  4. Revoke the old key via DELETE /v1/api-keys/{id}.

Rotate when:

  • A team member with access leaves.
  • A key was committed to source control or otherwise exposed.
  • On a calendar schedule — we recommend quarterly.

Errors

Status Detail When
400 name required / name too long (max 120 chars) Bad create body.
401 invalid api key The Authorization: Bearer cat_pk_... secret (used against the runtime, not this CP resource) doesn't match any stored hash.
401 api key revoked The secret matches a key whose revoked_at is set.
403 not_your_key Tried to revoke a key belonging to another workspace.
403 plan_limit_exceeded Workspace hit its api_keys tier cap on create.
502 CP couldn't reach the runtime.

These are plain-text detail strings from the runtime's auth guard, not machine code values — there's no structured error-code taxonomy on this resource beyond not_your_key, which the CP itself raises.

Events

API-key CRUD isn't currently emitted as outbox events — reserved for a later release.

Security model

  • The secret is hashed (sha256) at rest; the row stores the hash plus a display-only key_prefix. The plaintext is never logged.
  • last_used_at is updated asynchronously; treat it as advisory, not authoritative for auditing.
  • Every key is scoped to the customer_id it was minted under — there's no cross-workspace surface for a leaked key to reach, independent of any user-count question.
  • No IP allowlisting in v1; if you need that, terminate the key at your own proxy and add it there.

Next