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.
That one travels in the request body of the endpoints that accept it, and in
the path of the two public reads addressed by it —
GET /v1/jwks/{publishableKey} and GET /v1/config/{publishableKey}, which a
sign-in screen and a backend fetch before anybody has signed in.
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, 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. Changing the
password from inside the account ends every other session and keeps the one
that made the change: the person doing it is the one who just proved the old
password.
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 by an email code. 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/{publishableKey}.
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.