Login (email + password)
Auth
Login (email + password)
Exchanges email + password for access + refresh tokens
POST
Login (email + password)
Overview
Logs in an existing user with email + password and returns an access token (RS256 JWT, 15-min TTL) plus a refresh token (256-bit opaque, 30-day TTL, single-use rotating). The user must have verified their email (emailVerified: true); if not, the response is 403 STATE_CONFLICT with detail: "Email not verified".
Authentication
None required — this is the endpoint that grants authentication.Path parameters
None.Query parameters
None.Request body
| Field | Type | Required | Allowed values | Example |
|---|---|---|---|---|
email | string | yes | RFC 5322 valid, lowercased server-side, 5..254 chars | alice@example.com |
password | string | yes | 1..200 chars (we do not constrain composition beyond minimum length; argon2id absorbs the cost) | correct horse battery staple |
Example payload
Response — 200 OK
| Field | Type | Notes | Example |
|---|---|---|---|
accessToken | string | RS256 JWT, 15-min TTL. Pass in Authorization: Bearer <accessToken>. | eyJhbGciOiJSUzI1NiIs... |
refreshToken | string | 256-bit opaque token, base64url, 30-day TTL. Single-use; rotates on every /auth/refresh. | v8q3..._43-chars-total |
user | object | See User object. |
User object
This is a lightweight summary for rendering the Account header immediately after login. For the full profile (includingdateOfBirth and bio), call GET /users/me.
| Field | Type | Allowed values | Example |
|---|---|---|---|
id | string | 24-char Mongo ObjectId | 6a22f1897f96f4bd18ab7168 |
email | string | lowercased | alice@example.com |
firstName | string | null | 1..100 chars; null until collected at signup | Alice |
lastName | string | null | 1..100 chars; null until collected at signup | Andersson |
avatarUrl | string | null | Public CDN URL of the profile photo, or null | null |
onboardingStep | string | Lowercased status: in_progress | complete | in_progress |
tenancyStatus | string | Lowercased: not_submitted | pending | approved | rejected | not_submitted |
subscriptionStatus | string | Lowercased: none | trialing | active | past_due | cancelled | free_launch | expired | none |
The status fields (
onboardingStep, tenancyStatus, subscriptionStatus) are returned lowercased strings in this payload. GET /users/me returns the canonical UPPERCASE enum values for onboardingStatus/tenancyStatus.Example response
Error responses
| Status | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_FAILED | Email is malformed, password is missing, etc. |
| 401 | UNAUTHENTICATED | Email + password combination is wrong (we do NOT distinguish between unknown email and wrong password — prevents user enumeration). |
| 403 | STATE_CONFLICT | Email not verified yet. |
| 403 | ACCOUNT_BANNED | User is banned. |
| 403 | ACCOUNT_PENDING_DELETION | Credentials are correct, but the account was soft-deleted and is still inside the 30-day erasure grace window. The body carries meta.deletedAt and meta.deletionScheduledAt. This is the signal to route the user to the reactivation screen — call POST /auth/reactivate. Only ever returned after the password is verified, so a wrong password still yields the generic 401 (no enumeration). |
| 429 | RATE_LIMITED | Too many login attempts from this IP. Respect Retry-After. |
Example error — 403 ACCOUNT_PENDING_DELETION
Example error — 401
Side effects
- Records
lastSeenAton the user document. - Creates a
refresh_tokensrow keyed by the new session id (ULID).
See also
- Authentication — full token lifecycle, refresh, logout, socket tickets.
- Errors — the stable error code catalog.