Email OTP login
Passwordless sign-in via 6-digit codes mailed to the user. Good for consumer-app flows where you don't want password management at all, or as a recovery factor for password-managed accounts.
Flow
POST /v1/auth/email-otp/request { email }
→ 204 No Content (always)
→ if email is registered: send "Your sign-in code is 123456"
POST /v1/auth/email-otp/verify { email, code }
→ 200 { status: "success", access_token, ... } // or
→ 200 { status: "mfa_required", mfa_token, ... } // if MFA enrolled
Properties
- Code format: 6 digits, zero-padded (
"042857"). - TTL: 10 minutes.
- Max attempts per challenge: 5. After 5 misses, the challenge is locked even if the right code arrives later.
- Storage: SHA-256 hash; plaintext exists only in the outbound email.
- Single-use: marked consumed on success.
Enumeration defense
/request ALWAYS returns 204 — for registered emails, unknown emails,
disabled accounts, and even malformed input. Account existence can't be
inferred from the response.
/verify returns the same authentication_required error for all failure
modes (unknown email, expired code, wrong code, attempts exhausted).
When the user has MFA enrolled
The verify endpoint returns status: "mfa_required" with an mfa_token,
not a session. The caller must POST to /v1/auth/mfa/challenge
to complete the flow.
What about password-reset?
Password reset uses a separate flow — see /v1/auth/password/forgot.
The mechanics overlap but the schema and TTLs differ.
Limitations
- Email-only. No SMS-OTP variant.
- No "magic link" alternative; the user always types the 6 digits.
- Resending a code wipes any prior outstanding code for the same user.
- Codes are NOT a step-up factor. Use
/v1/auth/mfa/verifyfor step-up.
Combining with passwords
A common pattern is to offer email OTP as ONE of the sign-in options:
<LoginForm>
<option>Email + password</option>
<option>Email me a code</option>
<option>Sign in with passkey</option>
</LoginForm>
Each option calls a different SDK endpoint; the end state (a session cookie) is identical.