The four families
uk_st_… — staff session
A person signed into the panel. Administers the organization. Lives 30 days,
revocable.
uk_sk_… — organization API key
Your backend. Reads and writes the customer plane, scoped to the key’s
environment.
uk_pk_… — publishable key
Your pages. Public by design; the allowed-origins list is its boundary.
uk_ct_… — contact session
One of your users. Reads their own contact and nothing else.
Authorization: Bearer header, except the publishable key,
which travels in the request body of the endpoints that accept it.
Nothing else is accepted as a credential. No query parameter — it would land in
access logs and browser history. No cookie — this API is not cookie-authenticated,
which is also why CORS never needs credentials.
Presenting the wrong one
A well-formed credential from another family gets told so, rather than a flat “invalid”:401
Staff sessions
uk_st_…, minted by /v1/auth/signup, /v1/auth/login and
/v1/auth/two-factor. Valid for 30 days.
Long, because it is revocable. Signing out, changing the password, resetting
the password or losing the membership all kill it server-side — unlike a
self-contained JWT, which stays valid until it expires no matter what happens to
the account.
Organization API keys
uk_sk_…, server-side only. Created in the panel, shown once, stored as a hash.
revoked_at rather than deleting the row, so the audit trail of
which key existed does not disappear with it.
Publishable keys
uk_pk_…, one per environment, seeded with it, stored in plaintext — because
a publishable key identifies and authorizes almost nothing, and the panel has to
be able to show it again.
It sits in your page’s HTML by design. What keeps a stranger who read it from
flooding your tenant with junk contacts is the allowed-origins list on its row,
plus a per-key rate limit on boot.
Origin header is refused too: browsers always
send one cross-origin, so “no Origin” means “not a browser” — exactly the caller
the list exists to stop.
Contact sessions
uk_ct_…, your user’s own credential, minted by boot, by a redeemed magic link
or email code, by hosted sign-in, or by email verification. Valid for 30 days.
A contact session reads its own contact and nothing else. That scope is the whole
authorization model of uk_ct_.
verified flag — whether the identity behind it was proven:
An unverified session is usable in development and marked in the panel, but it is
barred from anything another person’s data could leak through.
A contact can see and end their own sessions —
GET /v1/contact/sessions,
DELETE /v1/contact/sessions/{id}, and DELETE /v1/contact/sessions for
everywhere-else. That is part of the token’s safety case, not a nice-to-have: it
lives on your domain without httpOnly.
The one non-opaque credential
Everything above is opaque — a random string whose meaning lives in our database. The exception is the session JWT: five minutes, ES256, signed per environment, minted from auk_ct_… session at POST /v1/contact/token and verified by your
backend offline against GET /v1/jwks/{publishable_key}.
It exists so your API does not have to call ours on every request. It is short
precisely because it cannot be revoked. See
Session tokens.
One-time tokens
Not credentials you hold, but the same shape — one prefix each, so none can be mistaken for a session:What is stored
A database dump hands out no working credentials.