Docs

Authentication

The two-token model: partner backends hold an application token, browsers hold a short-lived access token.

Overview

Two credentials, two questions: who is the partner, and which person is logged in.

Consumer authentication is an on-behalf-of model. Your backend holds a long-lived application_token — a server-to-server credential that identifies your tenant and must never reach the browser. The consumer's browser holds a short-lived access_token identifying the person. Consumer-facing calls carry both, and KeenAgents cross-checks that the access token belongs to the same tenant as the application token before anything proceeds. Forgot-password is the one single-token flow — the user is logged out, and the emailed link proves ownership instead.

the two headers

Authorization: Bearer <application_token>    # who the partner is (server-to-server, never in the browser)
X-Access-Token: <consumer access_token>      # which person is logged in (short-lived, browser-held)

Both tokens are signed and are verified on every request — nothing is trusted from an earlier call. Everything sensitive inside a consumer token travels encrypted: treat tokens as opaque strings, pass them in the headers above, and never try to parse or store their contents.

Core Concepts

application_token
Your backend's credential, obtained by exchanging your client_id and secret credentials at POST /v1/auth/application_token. It identifies your tenant on every consumer route and is re-verified against your organization's current API-key bindings on each call — so deleting or rotating the API key it was issued for invalidates it immediately, with nothing to wait out.
access_token / refresh_token
The consumer's browser credentials, issued at login as a pair: a short-lived access token (default 10 minutes) and a longer-lived refresh token (default 30 days). The access token accompanies every consumer call; the refresh token is used only to obtain a fresh pair, and each refresh token works exactly once.
Origin binding
Consumer tokens are issued for the exact web origin that requested them and are only accepted when presented from that same origin. Your organization maintains an allow-list of origins; requests from unknown origins are refused without explanation. Why: a stolen token is useless from anywhere except the site it was minted for.
Emailed secure links
Registration, password-reset and account-delete flows confirm through an emailed link. The link carries an encrypted payload only your tenant's secret can open, it is single-use, and it expires shortly after being issued. Why: anyone who merely sees the email learns nothing, and a forged or replayed link can never mutate an account.
Rotation kill-switch
Every consumer call checks the token against your tenant's current credentials. Rotating your contract key from the admin console therefore instantly invalidates every outstanding consumer token for your tenant — consumers simply log in again. Why: revocation must take effect at once, not when tokens happen to expire.

Rules & Limits

Exact lifetimes, formats, and refusal semantics.

  • Access token lifetime: 10 minutes by default. Refresh token lifetime: 30 days by default. Plan the browser session around refreshing, not around a long-lived access token.
  • A refresh spends the refresh token: the old one stops working the moment a new pair is issued. Serialize refresh calls in your backend — two concurrent refreshes send the same already-spent token and the loser receives a 401 that looks like a logout.
  • Tokens are opaque. Do not decode, inspect, or persist their contents — everything sensitive inside is encrypted, and the internal layout may change without notice.
  • The tenant seal secret (set on your certificate in the admin console) must be 16–32 characters. It protects the emailed secure links for your tenant; leaving it unset leaves those links effectively unprotected.
  • A request with a missing or unregistered origin, or with credentials that no longer match a fully configured API key, is refused with 401. There is no fallback for a missing origin.
  • Application-token failures on consumer routes all answer a uniform 401 Invalid token, whatever actually failed. Why: a caller probing with bad credentials should learn nothing about which check stopped it.
  • Same-tenant cross-check: when the access token does not belong to the application token's tenant, the call is refused with 403 Token does not belong to this tenant. A missing X-Access-Token answers 401; an API key with no consumer contract bound answers 403 Action forbidden.
  • OTP: a 6-digit pin emailed to the consumer, valid for 2 minutes (plus a small grace window), and spent by a single confirm attempt. The pin is bound to the tenant it was issued under, so a leaked pin cannot be confirmed anywhere else.

Login, OTP & Refresh, End to End

From the password POST to a rotated token pair.

  1. Your page POSTs /v1/auth/access_token with the consumer's email + password from an allow-listed origin. Requests from unknown origins are refused without a response.
  2. KeenAgents verifies the password against the account for that email under your tenant. Email is unique per tenant, so the same address registered with two partners always resolves to the right account.
  3. Your tenant's policy applies: login kinds your contract blocks are refused with 406 Login is not allowed for this account., the consumer's spaces are clamped to those your contract currently allows, and whether OTP is required is read from your contract's policy.
  4. If OTP is required, the reply is a Verify challenge while a 6-digit pin is emailed to the consumer; POSTing /v1/auth/otp/confirm with the pin and the challenge releases the token pair. Note: the organization-wide OTP setting must also be on for confirms to succeed.
  5. Otherwise the access + refresh pair is issued directly.
  6. Refresh (/v1/auth/refresh_token): if the current access token is still valid, the reply is Token is still valid! and nothing rotates — keep the tokens you have. Otherwise a new pair is issued carrying the consumer's live spaces and tenant binding, and the old refresh token is spent.
A rotation SPENDS the refresh token
Two concurrent refresh calls send the same already-spent token and the loser gets a spurious 401 that looks like a logout. Partner backends must serialize their refresh calls.

Per-Request Verification

Nothing is trusted from the connection — every message proves itself.

On the live chat connection, the access token is verified on every message, not once at connect. The requested space must be both in the consumer's own grant and currently allowed by your contract — revoking a space stops it working immediately, not at token expiry. KeenAgents also determines the tenant from the verified credentials on every message, overwriting anything the client sends — a client can never choose its own tenant partition. Why: a connection can outlive logins, policy changes and rotations, so no message may ride on yesterday's proof.

token verified per messagetenant enforced server-sideE3002 does not close the socketE3101 on key rotation

An expired token mid-conversation returns error E3002 but leaves the connection open — refresh the token and retry on the same socket. The connection outlives the token by design. A rotated contract key surfaces as E3101 on every outstanding token for your tenant; re-login is the recovery.

Good to Know

  • The Origin header must match the origin the token was issued for on every subsequent call. A partner serving the same SPA from two origins will see confusing "re-authenticate" rejections on otherwise-valid tokens — check which origin the token was minted under before suspecting a rotation.
  • A rejected Origin is refused silently — no status, no body; callers see an empty reply or a generic gateway error. It looks like an outage but is the allow-list working. Browsers also omit Origin on URL-bar navigation, so pasting an API URL into the address bar always fails.
  • An empty space intersection does not block login: the consumer gets a valid token with zero spaces and every run then fails space authorization (E3101). If a consumer "can log in but nothing works", compare your contract's allowed spaces against the account's spaces.
  • Refusal messages on the auth surface are deliberately general — they name the failure class, never the specific check. When an integration fails, work from this page's rules rather than expecting the response to explain itself.
Set a real tenant seal secret
A certificate without its own seal secret leaves your tenant's emailed registration and reset links effectively unprotected. Setting a real 16–32 character secret per tenant is mandatory, not optional.

Previous

Overview

Next

Live Chat

Keen Agents 2026

Documentation

Release 15