Docs

Contracts

Per-tenant rules for a partner's consumers: registration, OTP, login blocking and AI space assignment.

The Tenant Contract

One contract per partner: identity, policy and space assignment in a single record.

A consumer contract defines how one partner's consumers may use KeenAgents. It answers three questions: who they are (a name plus a 19-character tenant key, unique across the whole platform), what they may do (five policy toggles covering registration, OTP and login blocking), and which AI spaces they may use (a master set plus two default lists). A contract is bound 1:1 to at most one API key — the binding is set from the API key side, via the "Pick a consumer contract" dropdown on the API key form.

Consumers belong to their contract. Every consumer registered under a contract stays attached to it for its whole life: the contract's policy governs how the consumer signs in and its allowed-spaces set caps what the consumer may use. Deleting a contract does not delete its consumers — they are released into an unassigned state and an admin re-assigns each one to a contract from the consumer's own page. After changing a contract, use the bound API key's Wake action to send the partner a refresh notice so it re-fetches the contract.

Core Concepts

Contract key
Exactly 19 alphanumeric characters (^[A-Za-z0-9]{19}$), unique across the platform. Either admin-supplied or auto-generated when omitted at create. The key is the tenant identity the partner holds — consumer sessions are minted under it, which is why re-keying acts as a per-tenant kill-switch (see below).
Space sets
The master spaces set is what the contract may offer. Two default lists — spaces auto-assigned to system-registered and to outside-registered consumers — are enforced subsets of the master set. Defaults shape future registrations; the master set governs everyone on the contract.
Partner notifications
Notifications reach the partner through the Webhook entry bound to the same API key, not through the contract itself. After a policy or space change, Wake the key: a refresh notice lands on the entry's notification URL, carrying no payload — the model is notify-then-fetch: on receipt, the partner re-fetches the contract through the authenticated API.
API key binding
A contract can be bound to at most one API key; deleting the contract clears the key's binding rather than deleting the key. While no contract is bound, KeenAgents refuses to mint application tokens for that key — the binding is a hard prerequisite for partner access.

Reading Your Contract Over the API

Two partner-facing reads — the contract's identity and spaces, and its live policy — both scoped to your own key.

A partner never changes a contract over the API — that is the admin's job — but it does read two resources from it, both authenticated with your application token and served scoped to the api key that token was minted for. You only ever see your own tenant's contract. Both are the notify-then-fetch targets a Wake nudges you to re-read.

GET /v1/resources/consumer-contract — your identity + the spaces you may use

GET /v1/resources/consumer-contract
Authorization: Bearer <application_token>
Origin: https://your-site.example200 {
  "success": true,
  "status": 200,
  "message": "Success",
  "result": {
    "id": "…",
    "apiKeyId": "…",
    "name": "Acme Store",
    "key": "aB3dE7gH1jK4mN6pQ9r",                 // your 19-char tenant key
    "createdAt": "2026-01-14T09:22:03.114Z",
    "spaces": [ { "id": "…", "title": "Support" } ]
    // the five policy booleans also appear here but read the mapped
    // DEFAULTS, not the live values — use consumer-policy below for those
  }
}

// no contract bound to your key yet:200 { "success": true, "status": 200, "message": "Success", "result": null }

GET /v1/resources/consumer-policy — the LIVE registration / login policy

GET /v1/resources/consumer-policy
Authorization: Bearer <application_token>
Origin: https://your-site.example200 {
  "success": true,
  "status": 200,
  "message": "Success",
  "result": {
    "id": "…",                                      // your consumer contract id
    "key": "aB3dE7gH1jK4mN6pQ9r",
    "outsideRegistration": true,
    "otpInsideRegistration": true,
    "otpOutsideRegistration": true,
    "blockOutsideRegistrationLogins": false,
    "blockSystemRegistrationLogins": false,
    "defaultSystemSpaces": [ "…" ],
    "defaultOutsideSpaces": [ "…" ]
  }
}

// no policy bound to your key yet:200 { "success": true, "status": 200, "message": "Success", "result": null }
Two reads, one authority on policy
The contract read carries the policy booleans as their mapped defaults — treat it as authoritative only for the contract's identity and its spaces. For the live registration and login rules — including the two default-space lists new consumers receive — read /v1/resources/consumer-policy.

Failure modes — both reads

200 · result: null
Not an error. Your api key has no contract (or policy) bound yet — the request succeeded and returned an empty result. Wait for the admin to finish binding, then re-read.
401 Unauthorized
{ "message": "Invalid token", "error": "Unauthorized", "statusCode": 401 } — the application token is missing, malformed, or no longer matches its api key / client binding. Mint a fresh application token and retry.
Connection reset / no reply
No descriptive error at all — the request's Origin header is not on the allow-list, so the connection is dropped silently (through a proxy it surfaces as a generic gateway error). Check the Origin your site is served from matches the one registered on your webhook entry.

The Five Policy Toggles

Submitted as a full replace-set — always all five, never a patch.

FlagDefaultWhat it governs
outsideRegistrationtrueAllow partner-side consumer registration.
otpInsideRegistrationtrueRequire OTP for system-registered consumers.
otpOutsideRegistrationtrueRequire OTP for outside-registered consumers.
blockOutsideRegistrationLoginsfalseBlock logins for outside-registered consumers.
blockSystemRegistrationLoginsfalseBlock logins for system-registered consumers.
There is no systemRegistration flag
In-app (system) registration rights are governed by org roles, not the contract. The contract only decides whether the partner may register consumers from its own side, and how those consumers log in afterwards.

Rules & Limits

  • Name: required, 1–100 characters, unique.
  • Key: exactly 19 characters matching ^[A-Za-z0-9]{19}$, unique across the platform; auto-generated when omitted. A bad shape is refused with Key must be exactly 19 characters from A-Z, a-z and 0-9.
  • Org-wide cap: 20 contracts, checked on create only — one past the cap returns 400 with a Reached the maximum number of … message; delete one first. The cap keeps each organization's partner surface small enough to audit at a glance.
  • Policy updates submit all five toggles as a full replace-set — there is no partial patch, so the saved policy is always exactly what was last submitted.
  • Space assignment bodies are a spaceIds array, max 500 entries, with replace-all semantics (the full desired set, not a delta). An unknown space id → 400; a default list that is not a subset of the master set → 406.
  • There is no rotate action for contracts — but editing the key is effectively a rotation (see the warning below).
  • Refresh notices are best-effort and delivered once, without retries. Because they carry no payload, a missed notice costs nothing that a re-fetch of the contract does not recover — never act on the notice itself.
  • Contract changes take effect immediately, everywhere across the platform.

What Changing a Contract Does

Replacing the master space set, step by step.

  1. The admin replaces the contract's master spaces set (replace-all, max 500 ids; every id must be a known space).
  2. Both default lists are automatically pruned down to the new master set, so the subset rule always holds.
  3. KeenAgents prunes every existing consumer on that contract down to the new set — removed spaces drop off live consumers promptly, not at next login, so revoking a space takes effect while sessions are still open.
  4. The admin Wakes the bound API key so the partner receives a refresh notice and re-fetches the contract.
  5. Editing only a default list does not affect existing consumers — defaults shape future registrations only.
Editing the 19-character key signs every consumer out
Consumers stay attached to the contract itself, so re-keying does not detach them — but their sessions were minted under the old key, so every signed-in consumer of the tenant is refused immediately and must log in again. That makes the key field a deliberate per-tenant kill-switch; other tenants are untouched.

In the Admin UI

Where contracts live and what the forms enforce.

  • Consumer Contracts is a sub-page of the API Keys sidebar section. Access follows the API Keys permission: read access to view contracts, write access to change them.
  • The detail page carries the five policy toggles (Yes/No), the three space-assignment lists (master spaces plus the two default lists, pickers sourced from the live spaces list), and the 19-character key field with a Generate button for rotation.
  • The binding is made on the API key form — its "Pick a consumer contract" dropdown offers unassigned contracts plus the currently bound one on Edit.
  • Guardrails surfaced by the forms: the 20-per-resource cap, unique names and keys (refused with a friendly 400 and a clear message), and the defaults-must-be-subset rule (406 on violation).
  • A Rebuild Resource Cache button on the detail page rebuilds this contract's served configuration — both the contract data and its consumer policy — 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.
Consumer contract detail page

A consumer contract's detail screen — the five policy toggles, the assigned-spaces lists, and the 19-character tenant key.

Deleting a contract releases its consumers and cuts the partner off
The bound API key survives — only its contract binding is cleared — but the key stops being fully bound: its Origin drops out of the partner-facing allow-list and application-token requests are refused with api_key has no consumer contract bound (401) until a contract is re-bound. The contract's consumers are not deleted: they are released into an unassigned state, cannot sign in through any partner site, and wait for an admin to re-assign each one to a contract from the consumer's own page.

Previous

Certificates

Next

Webhooks

Keen Agents 2026

Documentation

Release 15