Docs

Api Keys

The partner credential at the center of the access chain — issued once, shown once, rotatable.

The Credential at the Center

One record links everything a partner needs.

An API key is the core record of the API access chain. The raw key — it starts with kak_ — is itself the Bearer credential for direct API calls, with no token-exchange step in between. That is what makes revocation immediate: deleting a key cuts access right away. KeenAgents keeps only an irreversible hash of the key; lists and detail pages show a display prefix (kak_ plus the first 8 characters), never the full key.

Beyond being a credential, the key is the chain's hub: it binds four sibling resources, each 1:1 and each set through a picker on the key form — a Client (the partner server's identity pair), a Certificate (the tenant's signing material), a Consumer Contract (the per-tenant policy), and a Webhook entry (the named pairing of the partner's browser Origin and its notification URL). Because webhook origins are unique, an inbound request's Origin header always resolves to exactly one key. The other chain members have their own pages; this one covers the key itself.

Core Concepts

Reveal-once
The plaintext key is returned exactly once — after create (and once per rotation) — in a copy-to-clipboard reveal panel. It can never be recovered afterwards: only the irreversible hash and the 8-character display prefix persist, so even KeenAgents cannot read the key back. A lost key means rotate or recreate; there is no recovery path.
Bindings
A Client, a Certificate, a Consumer Contract and a Webhook entry can each be bound to at most one key, so every binding is unambiguous. All four are optional at create — a key can exist half-wired. Deleting a bound resource unbinds it from the key rather than deleting the key — the key remains as a partial row you can re-wire.
Fully bound
The rule that keeps half-wired keys invisible to partners. A key works for its partner only when all four bindings are wired — its webhook entry's Origin joins the allow-list, tokens can be minted, and the key can be Woken. Unbinding any of the four switches the partner off the same way. The admin list is deliberately unfiltered so operators can see and finish partial rows.
Wake
The action that tells the partner to refresh: it sends a notice to the key's webhook entry notification URL, and the partner re-fetches its configuration over the authenticated API (notify-then-fetch — the notice carries no data). Wake is available only while the key is fully bound; use it after you finish or change the wiring.

Fully bound =

client boundcertificate boundcontract boundwebhook bound

Rules & Limits

Exact formats and caps you will hit in the forms.

  • Key format: kak_ followed by a long random secret. After creation only the display prefix (kak_ + first 8 characters) is ever shown again.
  • Name: required, 1–100 characters, unique.
  • Bindings: all four (Client, Certificate, Contract, Webhook) are optional at create. On update a new value re-binds, null unassigns, and omitting the field leaves the current binding unchanged. The partner's Origin and notification URL live on the bound Webhook entry, not on the key.
  • Organization-wide cap: 20 API keys, checked on create only — past the cap the create fails with a Reached the maximum number of … message; delete a key first. Keys have no owner account; there is no per-account allocation.
  • Propagation: every change — including a key flipping usable or unusable for its partner — takes effect immediately.

Rotation — What It Cuts, What It Doesn't

Rotate replaces the credential in place; delete revokes everything.

  1. An admin triggers Rotate from the key's detail page (confirmation dialog).
  2. A new plaintext key is minted: the record keeps its identity but the credential is replaced, and the new plaintext is revealed once.
  3. The old plaintext stops working immediately for direct raw-key calls.
  4. Outstanding application tokens keep working: they are tied to the key's identity, which rotation deliberately preserves, so a token issued before the rotation still verifies.
Rotation does NOT revoke application tokens
Rotation only cuts off direct raw-key (kak_) usage. Application tokens do not expire on a clock, so the only per-key lever that revokes them is DELETE — or unbinding the key's certificate or consumer contract so the chain check fails. Plan revocation around state, not time.

Reading Live Config — The Fetch After a Wake

Wake is notify-then-fetch: the notice carries no data. These are the two authenticated reads the partner makes to pick up what changed.

Both reads carry the partner's Authorization: Bearer <application_token> and are scoped to the caller's own API key — the token's subject — so a partner only ever reads its own tenant's configuration. There are no query parameters and no request body; the request must still arrive from an allow-listed Origin.

GET /v1/resources/consumer-contract — the tenant's master spaces

Returns the API key's bound consumer contract: its name, its partner-shared key, and the master spaces the tenant may offer. result is null when the key has no contract bound.

200 — consumer contract

GET /v1/resources/consumer-contract
Authorization: Bearer <application_token>

200 OK
{
  "success": true,
  "status": 200,
  "message": "Success",
  "result": {
    "id": "<consumer contract id>",
    "apiKeyId": "<this api key's id>",
    "name": "Acme production",
    "key": "<19-char partner-shared tenant key>",
    "createdAt": "2026-01-04T10:22:31.000Z",
    "spaces": [{ "id": "<space id>", "title": "Support" }],
    "outsideRegistration": false,
    "otpInsideRegistration": false,
    "otpOutsideRegistration": false,
    "blockOutsideRegistrationLogins": false,
    "blockSystemRegistrationLogins": false
  }
}
// result: null when the key has no contract bound.
// The policy booleans here are mapped DEFAULTS, not the live values —
// read /v1/resources/consumer-policy for the authoritative ones.
Policy booleans here are defaults, not live values
The five registration/login booleans on this response carry their mapped defaults — the served contract record omits the live policy. For the authoritative values read GET /v1/resources/consumer-policy below.

GET /v1/resources/consumer-policy — the live policy

The fetch leg of a r_consumer_policy Wake. Returns the live five registration/login booleans plus the two default-space subsets a new consumer is seeded with. result is null when the key has no contract bound.

200 — consumer policy

GET /v1/resources/consumer-policy
Authorization: Bearer <application_token>

200 OK
{
  "success": true,
  "status": 200,
  "message": "Success",
  "result": {
    "id": "<consumer contract id>",
    "key": "<19-char partner-shared tenant key>",
    "outsideRegistration": true,
    "otpInsideRegistration": false,
    "otpOutsideRegistration": true,
    "blockOutsideRegistrationLogins": false,
    "blockSystemRegistrationLogins": false,
    "defaultSystemSpaces": ["<space id>"],
    "defaultOutsideSpaces": ["<space id>"]
  }
}
// result: null when the key has no contract bound.
// These booleans are the LIVE policy values.

Failure modes (both reads)

no response / 502
The Origin is not on the allow-list. The connection is dropped before authentication — no status, no body — surfacing as a 502 through a gateway. Call from an allow-listed Origin.
401
A missing or malformed Authorization header, or an invalid token — Invalid token. Also returned when the token's API key was deleted, rotated, or re-bound to a different client since the token was minted (the binding check fails). Recover by re-minting an application token against the current key.
200, result: null
Not an error — the key simply has no contract bound. In practice a token only mints against a fully-wired key, so a live partner rarely sees it; treat it as "finish wiring the key", an operator task.

In the Admin UI

  • API keys live under the API Keys sidebar section. A single permission flag gates the whole surface: api-keys for read, api-keys:w for write — one role grant unlocks keys, Clients, Certificates, Consumer Contracts and Webhooks together.
  • The list page shows each key's name and kak_ display prefix — never the full key.
  • The create form takes a name plus four pickers — Pick a client, Pick a certificate, Pick a consumer contract and Pick a webhook; the pickers offer unassigned resources plus the currently bound one on Edit.
  • The detail page carries a Rotate dialog, a two-step Delete dialog and the Wake action (enabled only while the key is fully bound); the plaintext key appears exactly once, in a copy-to-clipboard reveal panel after create or rotate.
  • Half-wired keys are fully visible in the admin list on purpose — it is the only place to see and finish partial wiring; partners never see them until the key is fully bound.
  • The detail page also has a copy button for the Api Key ID — the stable identifier the sibling Client, Certificate, Consumer Contract and Webhook resources bind against.
  • A Rebuild Resource Cache button on the detail page rebuilds this key's served configuration on demand — a shortcut to the same action on the Cache page. It is gated by the cache permission (cache read, cache:w write), separate from the edit permission, so a cache manager can refresh it while an editor without cache rights never sees the button. You rarely need it — normal saves refresh the copy automatically. See Resource Cache.
API key detail page

An API key's detail screen — its name and display prefix, the four resource bindings, and the Rotate, Delete and Wake actions.

Good to Know

  • Deleting a bound Client, Certificate, Consumer Contract or Webhook entry silently unbinds it from the key — the key stops being fully bound and its partner loses access until re-bound. The admin UI still shows the key as a partial row.
  • A partner with valid credentials but a half-wired key cannot mint an application token: the request fails with the same uniform 500 Invalid request every credential problem returns. The specific reason (for example api_key has no certificate bound) is recorded server-side only, never disclosed to the caller — it is an operator wiring task, not a partner-side outage.
  • Origin matching is exact on scheme://host[:port], taken from the key's bound Webhook entry: the port matters (browsers omit only the default ports 80/443) and wildcards are not supported. A rejected Origin produces no error at all — KeenAgents simply drops the connection, so the caller sees a connection reset or a generic gateway error; this is deliberate, so an unknown caller learns nothing from the refusal.
  • After changing a key's wiring, use Wake to send the partner a refresh notice at its webhook entry's notification URL. The notice carries no data and is delivered once, without retries — the model is notify-then-fetch: the partner treats it as a hint and re-fetches over its authenticated API connection rather than acting on the notification alone.
Reveal-once is absolute
Only the irreversible hash and the 8-character display prefix survive creation. If the plaintext is lost, rotate the key and hand the partner the new one — there is no other way back.

Previous

Debugging

Next

Clients

Keen Agents 2026

Documentation

Release 15