Verify email (OTP)
Auth
Verify email (OTP)
Confirm a 6-digit verification code and receive access + refresh tokens.
POST
Verify email (OTP)
Overview
Verifies the 6-digit OTP that was sent to the user’s email during registration (or viaPOST /resend-verify-email). On success:
- The OTP row is marked consumed.
users.emailVerifiedis set totrue.- A new session is minted — access token (RS256 JWT, 15-min TTL) + refresh token (256-bit opaque, 30-day TTL, single-use rotating).
attemptsUsed and return 401 INVALID_CODE. After 5 attempts the row is dead and further calls return 401 CODE_EXPIRED; the client must request a fresh code.
The endpoint is idempotent on already-verified users — a second call returns a fresh session without consuming an OTP. This keeps the client’s “verify → land in app” flow robust on network retries.
Authentication
None required — this endpoint is part of the login sequence.Path parameters
None.Query parameters
None.Request body
| Field | Type | Required | Allowed values | Example |
|---|---|---|---|---|
email | string | yes | RFC 5322 valid, lowercased server-side | alice@example.com |
code | string | yes | Exactly 6 digits | 482301 |
Example payload
Response — 200 OK
| Field | Type | Notes |
|---|---|---|
accessToken | string | RS256 JWT, 15-min TTL. |
refreshToken | string | 256-bit opaque, 30-day TTL, single-use rotating. |
user | object | See below. |
User object
| Field | Type | Allowed values | Example |
|---|---|---|---|
id | string | 24-char Mongo ObjectId | 6a22f1897f96f4bd18ab7168 |
email | string | lowercased | alice@example.com |
firstName | string | null | null until collected at signup | Alice |
lastName | string | null | null until collected at signup | Andersson |
avatarUrl | string | null | Public CDN URL of the profile photo, or null | null |
emailVerified | boolean | Always true on this response | true |
onboardingStep | string | normalized lowercase of OnboardingStatus | in_progress |
tenancyStatus | string | normalized lowercase | not_submitted |
subscriptionStatus | string | normalized lowercase | free_launch |
Example response
Error responses
| Status | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_FAILED | Email malformed, or code is not exactly 6 digits. |
| 401 | INVALID_CODE | Wrong digits. attemptsUsed was bumped; you have at most 5 wrong tries before the row dies. |
| 401 | CODE_EXPIRED | No live OTP for this email — either it expired (5-min TTL), it was already consumed, or the attempt cap was hit. Request a fresh code via /resend-verify-email. |
Example error — 401 INVALID_CODE
Side effects
- Marks the OTP row consumed (
consumedAt = now). - Sets
users.emailVerified = true. - Inserts a
refresh_tokensrow keyed by the new session id.
See also
- Register (email + password) — the step that issues the code.
- Resend verification email — get a fresh code after the TTL or attempt cap.