POST /v1/auth/email-otp/verify
Exchange a 6-digit code from /request for a browser session.
::: tip Auth Required: none. The email + code IS the credential. :::
Request
POST /v1/auth/email-otp/verify
| Header | Required | Notes |
|---|---|---|
Content-Type: application/json | yes | — |
Body
{
"email": "alice@example.com",
"code": "123456"
}
Response
200 OK — status: "success"
If the user does NOT have MFA enabled: a full LoginResponse::Success body.
Cookies set: identsphere_at, identsphere_rt, identsphere_csrf.
200 OK — status: "mfa_required"
If the user has MFA enabled:
{
"status": "mfa_required",
"mfa_token": "eyJ...",
"mfa_token_expires_in": 300
}
The caller must POST to /v1/auth/mfa/challenge with
the mfa_token plus a TOTP/recovery code.
Error responses
| Status | Code | When |
|---|---|---|
| 400 | invalid_input | Email or code missing / malformed. |
| 401 | authentication_required | Unknown email, expired code, wrong code, or attempts exhausted. (Same response across all failure modes.) |
| 500 | internal_error | DB or JWT failure. |
Example: curl
curl -X POST https://auth.example.com/v1/auth/email-otp/verify \
-H 'Content-Type: application/json' \
-c cookies.txt \
-d '{"email":"alice@example.com","code":"123456"}'
Notes
- Wrong codes increment the attempts counter on the latest live challenge. After 5 misses the challenge is locked even if the right code is submitted later — the user must request a new one.
- Disabled / deleted accounts can't sign in even with the right code.
- The session is minted at AAL1 unless MFA is enrolled (in which case the caller must complete the challenge for AAL2).