Per-tenant signing certificates — your partner verifies consumer tokens with the public PEM; the private half never leaves the platform.
What a certificate is and why every partner chain needs one.
A certificate is the per-tenant signing identity of the API access chain. KeenAgents generates the keypair for you: the platform signs your partner's consumer tokens with the private half, and the public certificate (PEM) is what your partner application uses to verify them. A certificate is bound 1:1 to at most one API key, and it is one of the four bindings a key needs to be fully bound: until a certificate is bound, the key's Origin never joins the partner-facing allow-list, and an application-token mint is refused with api_key has no certificate bound.
Visibility is split on purpose: the public PEM is always visible in the admin UI, while the private key is never displayed and can never be read back through any API or UI surface — that guarantee is what makes the public PEM safe to share. Certificates are self-issued for your tenant with a one-year validity, so partners should trust the PEM they receive directly (pin it) rather than expect a public certificate-authority chain, and you should plan a rotation before expiry.
16–32 characters) used to encrypt consumer material that both KeenAgents and your partner must be able to read. Because both ends need the same value, it stays visible on the detail page — recoverable by design, unlike reveal-once secrets. It is an independent credential: rotating the keypair does not change it, and vice versa.1–100 characters, unique.20 certificates, checked on create only. Past the cap the create fails with 400 and a "Reached the maximum number of …" message — delete one first.16–32 characters after trimming. Submitting '' (empty) is valid and clears the value; omitting the field on update leaves it untouched.api-keys flag covers certificates — api-keys for read, api-keys:w for write, shared with API keys, Clients, Consumer Contracts and Webhooks.Rotate swaps the keypair in place; delete tears the whole thing down.
Delete (as opposed to rotate) permanently destroys the certificate and its private key, and unbinds it from the API key — the key stops being fully bound, so its Origin drops off the partner-facing allow-list and the partner loses access until a new certificate is bound.
The two HTTP calls behind the handshake: issue a one-time code, then claim it. Both carry the partner's application token.
Every call is gated on an allow-listed Origin and the partner's Authorization: Bearer <application_token>. The client_id query must equal the client the token was minted for.
GET /v1/resources/certificate — issue a code
GET /v1/resources/certificate?client_id=kcid_…Origin (allow-listed) and Authorization: Bearer <application_token>. Query client_id required; no request body. A re-issue against the same key overwrites the previous code and restarts the 120 s window.200 — code issued
200 OK { "success": true, "status": 200, "message": "Success", "result": { "code": "<one-time code — URL-safe, ~43 chars, single-use, valid 120 s>" } }
Failure modes — GET
client_id, a missing/invalid/expired token, a revoked key, or a client_id that doesn't match the token — every failure drops the connection with no status and no body, surfacing as a 502 through a gateway. A probing scanner never even confirms the route exists. Recover by fixing the credential and retrying; the specific cause is never disclosed.POST /v1/resources/certificate — claim the code
POST /v1/resources/certificate?client_id=kcid_…Origin (allow-listed), Authorization: Bearer <application_token>, and Content-Type: application/json.code — string, required: the code returned by the GET issuance. Extra fields are rejected.POST /v1/resources/certificate
POST /v1/resources/certificate?client_id=kcid_… Origin: https://app.yourcompany.com // must be allow-listed Authorization: Bearer <application_token> Content-Type: application/json { "code": "<the code returned by the GET issuance>" } 200 OK { "success": true } // the redeem webhook fires server-side, in parallel
On 200 { "success": true } the code is consumed and the redeem webhook is dispatched to your registered URL in parallel — the success reply means the delivery went out.
Failure modes — POST
Origin is not allow-listed — the connection is dropped before authentication and surfaces as a 502 through a gateway.Authorization header, or an invalid / revoked token — Invalid token. Also returned (as { "success": false }) when the client_id is missing or doesn't match the token, when the code is wrong, expired, or already claimed, or when the API key ↔ client binding no longer holds. Codes are single-use, so a replay always fails — recover by requesting a fresh code with a GET and claiming that.code, sends it empty, or includes unknown fields — rejected with the validation reasons. Send exactly { "code": "…" }.api-keys / api-keys:w permission.16–32 characters) with a Generate button; the value is displayed on the detail page — it is recoverable by design.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.
A certificate's detail screen — the always-visible public PEM, the optional shared secret key, and the Rotate and Delete actions.
Keen Agents 2026
Documentation
Release 15