Browser email-code sign-in #
Use these endpoints to post an application user's address and emailed code from a custom page. Successful responses are HTML or redirects, with the browser keeping the cookies that bind and complete the sign-in.
This reference covers ordinary browser sign-in. Add sign-in to your app declares the accounts service and enables the email method. Native authorization and reauthentication have additional state; use their own flows rather than adding those fields to an ordinary sign-in.
Origin and browser state #
Send both requests to the same application's sign-in origin and environment, under /__account/. Use its custom domain when that is the active sign-in origin. A code and cookie from another application, environment, or hostname cannot complete this flow.
Both posts check Origin: if present, it must exactly equal the sign-in origin. Without Origin, the request needs Sec-Fetch-Site: same-origin. The browser supplies these headers for an ordinary form; a different origin is refused invalid_request with status 400.
An accepted start sets the opaque __Host-turnzero_cloud_signin_code binding cookie. It is host-only, HttpOnly, Secure, and SameSite=Lax, with Path=/. Keep cookies enabled and use the same browser for verification. Do not read or construct the cookie's value in your page.
Start: request a code #
Send POST /__account/email/start with Content-Type: application/x-www-form-urlencoded. Use the field names below; the address field is named address.
| Field | Required | Value |
|---|---|---|
address |
Yes | The person's email address, as a string. |
return_to |
No | The application path to open after completion. It follows the return-path rules and is at most 512 characters. |
An accepted request returns status 200 and text/html: the code-entry page. It also sets the binding cookie above. The mail send completes separately, so this response does not prove that mail has arrived and does not return the code.
To send a new code, post the start form again with the address and the browser's cookie. Use the code from that request. A resend is subject to the emailed-code limits, so do not keep retrying a refused start.
Verify: submit the code #
Send POST /__account/email/verify as another form-encoded request. The browser must send the binding cookie from the accepted start.
| Field | Required | Value |
|---|---|---|
code |
Yes | The six digits as a string, preserving any leading zero. |
address |
No | The address to redisplay on a code-entry or resend form after a refusal. It does not choose the identity being verified. |
return_to |
No | Repeat the accepted start's value for the retry form. The successful destination remains the one stored at the start. |
Verification uses the binding cookie, not an address supplied in this second request, to find the pending code. Keep the start and verification in the same browser; a code without its binding cannot be confirmed.
Successful responses and redirects #
| Response to verification | What it means |
|---|---|
302 with Location |
The ordinary sign-in continues to its application return path. With no return_to, that path is /. Let the browser follow the redirect. |
200 with text/html |
A platform screen continues the flow, such as an account-linking choice or the passkey offer. Present that screen and follow its controls. |
A code proving an address does not bypass account admission: invitation-only sign-up, suspended accounts, and the realm's other checks still apply. A 200 response alone therefore does not establish that a session opened.
When the ordinary flow opens a session, the browser receives the __Host-turnzero_cloud_realm session cookie. There is no JSON success object or token response to extract from these two form endpoints. Read the signed-in user as Show the user and sign out describes.
Normal form navigation lets the browser render the returned pages and follow redirects. If your page uses fetch, inspect the status and Content-Type; do not call response.json() on every answer or parse HTML for a code or token.
Refused requests #
A wrong, expired, already used, or unbound code returns status 400 with an HTML code-entry page naming signin_code_invalid. It does not complete a new sign-in. Read the code again or use the page's resend control to start again when needed.
Some checks instead return application/json with contract_version, error, and detail: a disabled method can answer 400 route_not_enabled, an origin or return-path check 400 invalid_request, and the source's start limit 429 signin_rate_limited. Address validation, mail limits, and account admission can answer HTML refusal pages. Inspect the response type before choosing how to display it.
The sign-in guide's refusals and emailed-code limits give the causes and remedies, including signin_code_rate_limited and email_domain_undeliverable. Use the returned page's controls or named remedy; repeated posts do not bypass a limit.
Related #
- Add sign-in to your app configures sign-in and verifies a user's session in the backend.
- Make sign-up invitation-only controls who may create an account.
- Manage end users describes the realm's settings, passkeys, and code limits.
- Sign in from a native app uses the native authorization flow.
- Test sign-in with test end users exercises sign-in without a person receiving mail.