Skip to main content
/v1/boot proves the external_id and nothing else — an email that rides along is stored as an attribute, never as an identity edge. The two flows here are the ones that can prove the address itself, because they arrive in it. Each answers with a verified uk_ct_… session, sets email_verified on the contact, and publishes contact.email_verified. They are public surface reached with a publishable key in the request body, from your own domain, and they hold the API’s strictest posture: unconditional 202 on the request, one 401 for every way a redeem can fail, strict per-IP limits, and one-time tokens for anything that arrives by email.
Neither flow creates an account. The address has to belong to a contact already — one your backend created with POST /v1/contacts or that a boot carried — because a flow that minted contacts from an inbox would be a second sign-up door beside your own. The request still answers 202 for an address nobody has, so the endpoint cannot be used to learn which addresses do.
Passwordless sign-in for a page that has no session to offer: a ticket email, the public help center, the portal.
1

Request

Unconditional 202. Valid one hour. Rate limited to 5 per hour per IP.
2

Redeem

Answers the contact and a verified uk_ct_… session.
The redeem is atomic in SQL, so the link works exactly once. Used, expired and never-existed all answer the same 401 — three different truths, one response, on purpose.

Email codes

The same flow presented as six digits. Reach for it instead of a link when the code will be typed on the device that asked for it: a code survives a mail client that opens links in its own browser, and it works when the inbox is on a phone and the sign-in is on a laptop.
1

Request

202 — always
Hold the challenge in the page. It is not a session and can never be used as one. Valid ten minutes. Rate limited to 5 per hour per IP.
2

Verify

Answers the contact and a verified uk_ct_… session. The code arrived in the inbox, so it proves the address exactly like the link does.
The challenge comes back for every address, whether or not it has an account — otherwise this endpoint would be the place somebody learns which addresses do. The verify holds the same line: a wrong code, an expired one, a spent one, and a challenge that never had a code behind it all answer the same 401 invalid_code.
Two rules make six digits safe to accept, and both matter:
  • Each code carries five attempts. A guess costs one whether or not it was right, and the code dies when they run out. Six digits is a million, which a rate limit alone does not close.
  • Asking for a new code spends the outstanding one. Otherwise every request would add another five guesses against another number.
The code alone is not enough either: it is only valid against the challenge it was issued for, so a code read over somebody’s shoulder opens nothing without the browser that asked for it.

The session

Both flows hand back a uk_ct_… contact session, valid 30 days, with verified: true. Use it for the contact’s own reads:
Signing out deletes the session — the next lookup simply matches nothing. In the SDK the pair is requestEmailCode / signInWithEmailCode and requestMagicLink / redeemMagicLink on the client; the portal’s /sign-in is the code flow drawn by us.