Skip to main content
Your pages hold a uk_ct_… contact session. Your backend needs to know who is calling it, and asking us on every request would put our latency and our uptime in front of your API. So the contact session buys a short-lived JWT, signed per environment, that your backend verifies offline against a public key set.
Two credentials, two jobs. The opaque session is long-lived and revocable. The JWT is short-lived and unrevocable, which is exactly why it is short.

Minting

This is the refresh flow, and there is no second credential to manage: the opaque session is the refresh token. Call again whenever the last JWT is near expiry, for as long as the session lives.
Each mint records the contact against the month’s active-contact meter, idempotently. That is the number your bill reads — see the meter.

What the token carries

string
The environment id. Pin it. A test-environment token must not satisfy a live check.
uuid
The contact id.
uuid
The session id — what you pass to DELETE /v1/contact/sessions/{id}.
string
Your own id for this person, present when the contact has one. In federated mode this is what your backend keys on.
string
boolean
Whether the identity behind the session was proven. Gate on this before anything another person’s data could leak through — a verified: false token is a perfectly valid signature over an unproven claim.
integer
Unix seconds. The lifetime is 5 minutes.

Verifying

Fetch the key set once, cache it, and verify locally.
Addressed by publishable key — the one identifier your backend already holds in config. Public and unauthenticated by design: these are public keys. There is no Origin gate either, because the caller is a server and servers send no Origin.
Verify the signature. Do not read the payload without it — a JWT is base64, not encryption, and anything can be typed into one.

Cache the key set

The response carries:
That is deliberate. A verifier has to keep verifying through our bad five minutes, so a CDN or an in-process cache should keep answering while a refetch is retried. Most JWKS clients honour this for you; the ones above do. Keys are minted on first read, so the document is never empty for a configured environment. When JWT minting is unavailable on the server, the answer is an empty key set rather than an error — the shape stays valid, and POST /v1/contact/token is where you get the 501 jwt_unavailable.

Revocation, and the five minutes

A JWT is verified offline, so nothing can recall one. Deleting a session stops the next refresh, not the token already in a page. That makes five minutes the longest a revoked session keeps working — and it is the whole reason the long-lived credential is the opaque, revocable one and the short-lived credential is the signed one. If you need immediate revocation for a particular action, check the session server-side for that action rather than lengthening the JWT.

Sessions the contact can see

The contact session token lives on your domain without httpOnly. That makes “see my devices and end one” part of the token’s safety case, not a nice-to-have.
Revocation is deletion — immediate, with the JWT lifetime as the only tail. “Sign out everywhere else” keeps the session that asked. Another contact’s session id and one that never existed answer the same 404.

Rotating the signing key

Requires api_keys:write — the same gate as the identity secret, because this key mints the tokens your backend trusts. The old key stops signing immediately and keeps verifying for 24 hours: the overlap that lets JWKS caches refresh and in-flight tokens expire. Your backend needs no deploy — it refetches the key set and finds both kids. For a suspected compromise the arithmetic runs the other way: every token signed with the old key is dead within its 5-minute lifetime of the rotation. That is the bound to quote.

The active-contact meter

Requires customers:read. A contact counts as active for a month when a JWT is minted for them — so a session opened in January and refreshed in February is active in both. Live environment only, because only live contacts count at all. The month is UTC, decided once in the schema: display may translate it, a bill may not be ambiguous. This reads the same query the bill reads.