Skip to main content
Two-factor for contacts is the same machinery the panel gives your staff, one layer down: an authenticator app, single-use recovery codes, and a short-lived challenge between the two steps.
It is free, and it always will be. No authentication method sits behind a paid plan here — charging for the second factor makes the free tier a trap and security an upsell.

The decision worth knowing about

It is enforced wherever a session is minted. Password sign-in, magic link, email code, the verification link and social sign-in all stop at the same challenge. A second factor you can route around by clicking “continue with Google” is a checkbox, not a control — and the person who turned it on has no way of knowing which doors it actually covers. So it covers all of them.
A password reset does not clear it. Letting it would hand the bypass to whoever holds the inbox, which is most of what two-factor exists to stop.The consequence is real and you should plan for it: a contact who loses both their phone and their recovery codes cannot get back in on their own. There is no tenant-side override yet — see what is missing.

Enrolling

Two calls, from a signed-in contact session.
1

Setup

200
Render the QR; show the secret for anyone who cannot scan. This does not turn two-factor on.
2

Enable

200
Only now is it on. Waiting for a code that works means an abandoned setup — the tab closed halfway, the wrong app scanned — never locks anyone out.
The recovery codes are shown once. Show them once too, and say what they are for.

Signing in

Whichever door they came through, a contact with two-factor on gets a challenge instead of a session:
200
That token is not a session — it carries its own prefix precisely so it can never be spent as one — and it lives ten minutes.
The response is the ordinary sign-in response: contact, uk_ct_… token, expiry, verified. That last field carries whatever the first factor earned — a magic link and a social callback both prove the identity before the code is asked for, and that proof rides on the challenge rather than being recomputed after it. A recovery code goes in the same code field. It is spent when used, and a notice goes to the inbox — somebody using a backup code is worth noticing. A wrong code does not consume the challenge; fat fingers on six digits are the common case. It does spend one of the challenge’s ten attempts, and a challenge that runs out answers invalid_challenge from then on, whatever code arrives — the rate limit counts per address, and an attacker spreads guesses across addresses, so the budget is what actually bounds guessing a six-digit number. Ten is well above what a person mistypes; a person who hits it starts a new sign-in.

Turning it off

A code that currently works, TOTP or recovery. Not the password: a social-only contact has none, and the right proof for removing the second factor is the second factor. POST /v1/contact/two-factor/recovery-codes replaces the whole set the same way. Every previous code stops working — a partially-known set is worse than a fresh one.

Hosted mode only

Enrolling in a federated environment answers 409 environment_not_hosted. There, your own server authenticates and boot asserts the result: there is no moment we could interrupt to ask for a code. Two-factor is ours to enforce only where the sign-in is ours to run — if your product has its own MFA, that is where it belongs.

What is missing

There is no way for you to lift two-factor off a contact who lost both their phone and their recovery codes. That is deliberate rather than forgotten: the alternative on the table was letting a password reset clear it, which hands the bypass to whoever holds the inbox. It belongs with the support surface, audited — not with auth. Until it lands, the honest thing to tell your customers is to keep the recovery codes.