The partner credential at the center of the access chain — issued once, shown once, rotatable.
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.
Fully bound =
client boundcertificate boundcontract boundwebhook boundExact formats and caps you will hit in the forms.
kak_ followed by a long random secret. After creation only the display prefix (kak_ + first 8 characters) is ever shown again.1–100 characters, unique.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.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.Rotate replaces the credential in place; delete revokes everything.
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.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.
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.
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)
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.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.kak_ display prefix — never the full key — the Origin of its bound webhook entry, and how many of the four bindings it carries.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:
| Scope | Read lets you… | Write lets you… |
|---|---|---|
| API Keys | view issued keys and their bindings | create, edit bindings, rotate, Wake, delete keys |
| Clients | view clients | create, rename, rotate, delete client id / secret pairs |
| Certificates | view certificates and their public keys | create, edit the shared secret, rotate, delete |
| Contracts | view contracts, their spaces and policies | create, edit, delete; set policy and spaces |
| Webhooks | view webhook entries | create, edit, delete entries |
| Cache | see cache status on the Cache page | rebuild or invalidate — this is what shows the Rebuild Resource Cache button on every record 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.
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.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.Keen Agents 2026
Documentation
Release 15