Skip to main content
Every credential the API mints follows one shape: high-entropy random bytes behind a prefix that says what the token is, stored as a SHA-256 hash, shown in plaintext exactly once. The prefix does two jobs. It lets a leaked token be recognized — in a log, in a git push, in a repository scan — and it keeps a credential from one surface from ever opening another’s routes.

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.
All four ride the 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
That difference matters: one is a config mistake, the other is a security event, and they should not read the same in your logs.

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.
A user can see where they are signed in and sign out everywhere else:
“Everywhere else” keeps the session that asked — nobody wants to be logged out of the tab they clicked in.

Organization API keys

uk_sk_…, server-side only. Created in the panel, shown once, stored as a hash.
The environment is part of the prefix and, more importantly, part of the stored row: it is resolved from there on every request. See Environments. Revoking stamps revoked_at rather than deleting the row, so the audit trail of which key existed does not disappear with it.
A key in a browser bundle is a key in every visitor’s hands. uk_sk_ belongs on your server. For pages, use a publishable key.

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.
Matching is exact on the normalized origin — no wildcards, no prefixes, because the list is short and the ambiguity of anything cleverer is where bypasses live.
An empty list allows any origin. That is the getting-started state, and it is the one thing to fix before going live.
With a non-empty list a missing 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_.
It also carries a 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 a uk_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.