Accounts & auth¶
The self-service account flow: sign up, log in, and obtain the bearer tokens
every other endpoint expects. This is how you mint a JWT (eyJ…) without
an out-of-band invite. For long-lived, customer-facing integrations, exchange a
JWT for an API key once you're in.
For the auth model itself (JWT vs. API key, scopes, header format) see Authentication.
signup, login, and refresh are public (no token needed).
verify-email, resend-verification, and me require the access JWT from one
of those calls in the Authorization: Bearer … header.
The session object¶
signup, login, refresh, and verify-email all return the same envelope:
{
"session": {
"access_token": "eyJhbGciOiJFUzI1Ni␣…",
"refresh_token": "rt_8f3c…",
"token_type": "Bearer",
"expires_in": 3600
},
"user": {
"id": "019e3450-…",
"email": "ada@example.com",
"email_verified": false,
"created_at": "2026-05-17T03:14:00Z",
"last_login_at": null
},
"tenant": {
"id": "019e3451-…",
"display_name": "ada@example.com",
"plan_tier": "free",
"plan_status": "active",
"status": "active"
},
"email_dispatched": true
}
| Field | Type | Notes |
|---|---|---|
session.access_token |
string (JWT) | Send as Authorization: Bearer …. Short-lived. |
session.refresh_token |
string (opaque) | Exchange at /refresh for a new pair. Store securely. |
session.token_type |
string | Always "Bearer". |
session.expires_in |
int | Access-token TTL in seconds. |
user.id |
string (UUID) | Stable user id. |
user.email |
string | Normalised (lower-cased) email. |
user.email_verified |
bool | false until a code is consumed at /verify-email. |
user.created_at |
ISO 8601 | |
user.last_login_at |
ISO 8601 | null | null on a brand-new account. |
tenant |
object | null | Present only on /signup — the auto-provisioned Free-tier tenant. |
email_dispatched |
bool | null | Present only on /signup — whether the verification email was sent. |
Sign up — POST /v1/auth/signup¶
Public. Creates a user and an auto-provisioned Free-tier tenant, then returns a session and dispatches a 6-digit verification email.
Request¶
| Field | Type | Required | Notes |
|---|---|---|---|
email |
string (email) | yes | Validated; some domains may be disallowed. |
password |
string, 1–512 | yes | Must pass the strength policy. |
Response — 201 Created¶
The session object, with tenant and email_dispatched
populated.
Errors¶
| Status | reason |
When |
|---|---|---|
409 |
email_already_registered |
The email already has an account. |
422 |
password_too_weak |
Fails the strength policy (carries reason_code). |
422 |
invalid_email |
Not a parseable email. |
403 |
email_domain_not_allowed |
The domain is blocked (carries domain). |
503 |
signups_temporarily_paused |
Sign-ups are globally paused. |
Log in — POST /v1/auth/login¶
Public. Email + password → a fresh session.
Request¶
Response — 200 OK¶
The session object (tenant / email_dispatched omitted).
Errors¶
| Status | reason |
When |
|---|---|---|
401 |
invalid_email_or_password |
Wrong credentials (timing-flat). |
403 |
user_paused |
The user account is paused. |
403 |
tenant_paused |
The owning tenant is paused. |
Refresh — POST /v1/auth/refresh¶
Public. Exchange a valid refresh token for a new access + refresh pair (refresh tokens rotate on use).
Request¶
Response — 200 OK¶
The session object.
Errors¶
| Status | reason |
When |
|---|---|---|
401 |
invalid_refresh_token |
Unknown, expired, or already-rotated token. |
403 |
user_paused / tenant_paused |
Account or tenant paused. |
Verify email — POST /v1/auth/verify-email¶
Requires the access JWT. Consumes the 6-digit code from the sign-up email and
marks the account verified (the returned session reflects email_verified: true).
Request¶
| Field | Type | Required | Notes |
|---|---|---|---|
code |
string, 4–12 | yes | The code from the verification email. |
Response — 200 OK¶
The session object.
Errors¶
| Status | reason |
When |
|---|---|---|
410 |
invalid_or_expired_code |
Wrong or timed-out code. |
404 |
user_not_found |
The bearer's user no longer exists. |
Resend verification — POST /v1/auth/resend-verification¶
Requires the access JWT. No body. Reissues the 6-digit code (rate-limited).
Response — 200 OK¶
Errors¶
| Status | reason |
When |
|---|---|---|
404 |
user_not_found |
The bearer's user no longer exists. |
409 |
already_verified |
The email is already verified. |
429 |
resend_too_soon |
A code was sent too recently (carries message). |
502 |
email_delivery_failed |
The email provider rejected the send. |
Current user — GET /v1/auth/me¶
Requires the access JWT. Returns the bearer's identity and effective scopes — no database round-trip, so it's cheap to call on every dashboard load.
Response — 200 OK¶
{
"user": {
"id": "019e3450-…",
"email": "ada@example.com",
"email_verified": true,
"created_at": "2026-05-17T03:14:00Z",
"last_login_at": null
},
"scopes": ["charts:read", "charts:write", "compatibility:read"]
}
scopes is the sorted list of scope strings the access token carries — the same
set the API authorises each request against (see
Authentication › Scopes).