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 one of the three secrets your partner's server presents when it exchanges credentials for an application token; it is never sent on its own as a Bearer credential. 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 client, certificate or contract unbinds it from the key rather than deleting the key — the key remains as a partial row you can re-wire. A webhook entry is different: it cannot be deleted while a key holds it — clear it on the key first.
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 hands the partner everything the key carries. Wake sends two deliveries to the key's webhook entry notification URL: first the certificate material (the public certificate plus the shared secret key — the one thing a partner cannot fetch on its own), then a refresh notice listing every resource to re-read (key, consumer policy, spaces, agents, agent front settings). The partner stores the material and re-fetches the rest over the authenticated API. Wake is available only while the key is fully bound and saved; after changing anything bound to the key, press Rebuild Resource Cache first, then Wake.

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 presses Rotate beside the key prefix on the key's detail page and confirms (Confirm rotate).
  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: any new application-token request that still carries it is refused.
  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 retires the raw kak_ secret — future token requests must carry the new one. Application tokens do not expire on a clock, so the levers that revoke them are DELETE the key, or unbind any of its four bindings (client, certificate, contract or webhook): a key that is not fully bound drops out of the partner-facing set and its tokens stop verifying on the next request. Plan revocation around state, not time.

Rebuild, Then Wake

A partner reads everything through its API key, so the key's page is where you refresh a partner.

After changing anything attached to the key — its contract's spaces, defaults or policy, an agent in one of its spaces, its certificate, its client, its webhook entry — do two clicks on the key's page, in this order: Rebuild Resource Cache (rebuilds the served copy of every record bound to the key, and only those; it refuses a half-wired key with API key not found or not fully configured — bind its client, certificate, contract and webhook, then rebuild.), then Wake (delivers the certificate material and tells the partner which resources to re-read). Rebuild first so there is something fresh to fetch; Wake second so the partner fetches it. The two buttons are gated separately: Rebuild by Cache write, Wake by API Keys write.

What a Wake sends to the partner's notification URL

// 1 · the certificate delivery
POST <your notification URL>
Content-Type: application/json
{
  "synch-data": "r_cert",
  "type": "resource",
  "result": { "cert": "<public PEM>", "key": "<shared secret or ''>" }
}

// 2 · the refresh notice — every list to re-read
POST <your notification URL>
Content-Type: application/json
{ "synch-data": [ "r_api_keys", "r_consumer_policy", "r_space", "r_agent", "r_json" ] }

// Deleting your key sends the second POST once more, without the
// certificate: your next fetches answer 401 — that is how you learn
// you were revoked.

Each delivery is sent once, without retries. A partner that was down misses the POST and recovers by re-running the certificate handshake, or when you press Wake again.

Reading Live Config — The Fetch After a Wake

A Wake's refresh notice carries no data. These are the authenticated reads the partner makes to pick up what changed.

Both reads below 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.

Paths shown as /v1/… are reached at https://<your KeenAgents host>/keen-api/… — the /v1 prefix is replaced by /keen-api.

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 ids of 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": null,
    "name": "Acme production",
    "key": "<19-char partner-shared tenant key>",
    "createdAt": "2026-01-04T10:22:31.000Z",
    "spaces": [{ "id": "<space id>", "title": "" }],
    "outsideRegistration": true,
    "otpInsideRegistration": false,
    "otpOutsideRegistration": false,
    "blockOutsideRegistrationLogins": false,
    "blockSystemRegistrationLogins": false
  }
}
// result: null when the key has no contract bound.
// apiKeyId is always null and spaces carry ids only (titles come from
// GET /v1/space/list). The five policy booleans are fixed placeholders
// (outsideRegistration true, the rest false) — read
// /v1/resources/consumer-policy for the live values.
Policy booleans here are placeholders, not live values
The five registration/login booleans on this response are fixed placeholders — outsideRegistration reads true, the other four false — regardless of what the admin set. For the authoritative values read GET /v1/resources/consumer-policy below. Treat this read as authoritative only for the contract's identity and its space ids.

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

The fetch leg of a r_consumer_policy notice. 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.

The other two reads a Wake names

The refresh notice also names r_space / r_agent and r_json. The space list returns the organization's spaces with their agents — keep only those in your contract's spaces. The front-settings read is per agent, by slug, and answers 404 Agent not found for a slug outside your contract's spaces — the same message whether the agent is elsewhere or does not exist.

The other two reads

GET /v1/space/list                                     // every space with its nested agents[] — keep the ones in your contract
GET /v1/resources/agent-front-settings?agentId=<slug>   // one agent's partner-facing settings, by slug

Failure modes (all reads)

no response
The Origin is not on the allow-list. The connection is dropped before authentication — no status, no body — surfacing as a connection reset or a generic proxy error on your side. 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, lost one of its four bindings, or was re-bound to a different client since the token was minted. Rotating the key does not cause this — tokens survive a rotation. 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 sidebar's Api group as their own entry, next to Clients, Certificates, Contracts and Webhooks. Each entry has its own permission scope on the Roles page — API Keys, Clients, Certificates, Contracts, Webhooks — with read (see the section and open its records) and write (create, edit, rotate, Wake, delete). A role that wires a partner end to end needs write on all five; without read on a scope its entry simply does not appear. The binding pickers on the API key form need read on the sibling scope to list choices — without it the picker shows the current binding, locked, with Choices unavailable — requires … read permission.
  • The list page shows each key's name, its kak_ display prefix — never the full key — the Origin of its bound webhook entry, and how many of the four bindings it carries.
  • The create form takes a name plus four pickers, in this order — Pick a webhook, Pick a client, Pick a certificate, Pick a consumer contract; each is optional, offers unassigned resources plus the currently bound one on Edit, and has a "— No … —" option that leaves the binding empty (on Edit it unbinds).
  • On the detail page, Rotate sits beside the Key Prefix and arms a Confirm rotate / Cancel pair; the footer carries Save, Wake, Rebuild Resource Cache and a two-step Delete (Confirm delete / Cancel). Wake is enabled only when all four bindings are set and the form has no unsaved changes — hovering a disabled Wake tells you which binding is missing or that you must save first. A successful Wake confirms the notification URL it delivered to. 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, and the id your partner's server sends in the application-token exchange.
  • A Rebuild Resource Cache button in the footer refreshes the served copy of everything bound to this key in one click — the key itself, its certificate, its client, its consumer contract, that contract's consumer policy, the contract's spaces and the agents in those spaces — and nothing else, so other partners' records are untouched. It refuses, with a clear message, when the key is not fully configured as saved (all four bindings). On success the toast reads "Bound caches rebuilt — now Wake to push them to the partner" — click Wake next. The button appears only for roles with Cache write, independently of the API Keys scope. Use it after you change anything attached to the key — its contract's spaces or policy, an agent in one of its spaces, its certificate or client — as the first of two clicks: Rebuild, then Wake. See Resource Cache.
  • Also available to an AI agent through the Keen MCP Server: list_api_keys, get_api_key, create_api_key, update_api_key, rotate_api_key, wake_api_key, delete_api_key and rebuild_api_key_cache — same scopes, same rules.

The Api group's scopes, as a role grants them on the Roles page:

ScopeRead lets you…Write lets you…
API Keysview issued keys and their bindingscreate, edit bindings, rotate, Wake, delete keys
Clientsview clientscreate, rename, rotate, delete client id / secret pairs
Certificatesview certificates and their public keyscreate, edit the shared secret, rotate, delete
Contractsview contracts, their spaces and policiescreate, edit, delete; set policy and spaces
Webhooksview webhook entriescreate, edit, delete entries
Cachesee cache status on the Cache pagerebuild or invalidate — this is what shows the Rebuild Resource Cache button on every record page
API key detail page

An API key's detail screen — its Api Key ID and name, the four binding pickers, the key prefix with its Rotate control, and the Save · Wake · Rebuild Resource Cache · Delete footer.

Good to Know

  • Deleting a bound Client, Certificate or Consumer Contract 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. Deleting a bound Webhook entry is refused (Webhook "…" is bound to an api key. Unbind it there first.) — pick "— No webhook —" on the key, save, then delete.
  • A partner whose key is half-wired sees silence, not an explanation: until all four bindings are in place the key's Origin is not allow-listed, so its calls are dropped with no status (a connection reset or a generic proxy error on its side); where a call does get through, the token request answers the same uniform 500 Invalid request every credential problem returns. The specific reason (for example a missing certificate) is visible only to the admin, on the key's detail page — 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 proxy error on its side; this is deliberate, so an unknown caller learns nothing from the refusal.
  • After changing anything bound to a key, press Rebuild Resource Cache and then Wake on the key's page. Wake re-delivers the certificate material and sends one refresh notice naming everything the partner should re-read; each delivery is sent once, without retries, so a partner that was down simply asks again (the handshake) or waits for the next Wake.
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