These endpoints require the environment to be in
hosted mode. On a federated
environment they answer 409 environment_not_hosted.Sign up
202 — always
202. The response never reveals whether the address already has an
account. Three different things happen behind that one answer:
1
A new address
The contact is created with the password attached — there is nothing here to
take over — and a verification email goes out.
email_verified stays false
until the link is clicked.2
An address that already has a password
Nothing is created. A “you already have an account” notice goes to the
inbox, never to the caller.
3
A contact your server minted, with no password yet
The chosen password waits on the verification token and attaches only when
the link proves the email.
anonymous_id folds the visitor this person already was into the new
contact, attribution included.
Rate limited to 10 per hour per IP.
Verify the email
The link in the email carries auk_cv_… token, valid 24 hours. Your page reads it
from the query string and posts it:
200
Resend
202 always. Nothing goes out for unknown addresses or already-verified ones.
Rate limited to 5 per hour per IP — each request mails somebody’s inbox.
Sign in
200
401 invalid_credentials — and take roughly the same time, because the
no-password path runs a throwaway bcrypt compare. Without that, only real accounts
would pay the tens of milliseconds, and latency alone would answer “does this
address exist”.
Proving the password proves the account, so the session is verified.
Rate limited to 20 per 5 minutes per IP.
Recover a password
1
Ask
202. Rate limited to 5 per hour per IP.2
Reset
204. Rate limited to 10 per hour per IP.- every session of that contact is revoked, immediately — a reset usually means somebody else may hold the credential;
- the email counts as proven, because the link arrived in it;
- a contact that was never identified becomes identified.
This flow also works for contacts that never had a password — the ones your server
created with
POST /v1/contacts. Receiving the link proves the email, so “reset”
doubles as “set a password”.Magic links
Passwordless sign-in, and it works in both modes. In federated mode it is also the only thing that can upgrade an email from attribute to proven identity.1
Request
202. Valid one hour. Rate limited to 5 per hour per IP.2
Redeem
uk_ct_… session.401 — three different truths, one response, on
purpose.
Email codes
The same flow presented as six digits, and it works in both modes too. 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
2
Verify
uk_ct_… session. The code arrived in the
inbox, so it proves the address exactly like the link does.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.
Password policy
At least 8 characters, with a lowercase letter, an uppercase letter and a digit. Anything shorter or simpler answers400 weak_password.
Deliberately modest: length is what matters, and a policy people fight produces
Password1! everywhere.
And the password must not be a known-breached one. Every password set here —
sign-up, reset, “set a password” — is checked against a public corpus of
passwords that have appeared in breaches, and a match answers 400 breached_password. That single rule stops more account takeovers than any
composition requirement, because credential stuffing is what actually happens
to your customers.
The password does not leave our process, and neither does anything that
identifies it. It is hashed locally and only the first five characters of that
hash are sent; the corpus answers with every candidate under that prefix and the
comparison happens here. The service learns that somebody asked about one of
several hundred passwords, and nothing else.The check fails open. If the corpus cannot be reached the password is
allowed and we log it: refusing every sign-up because a third party is down
would be an outage we imported into your product.
The session
Every successful flow here hands back auk_ct_… contact session, valid 30 days,
with verified: true. Use it for the contact’s own reads: