Skip to content

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

{
    "email": "ada@example.com",
    "password": "a-strong-passphrase"
}
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

{ "email": "ada@example.com", "password": "a-strong-passphrase" }

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

{ "refresh_token": "rt_8f3c…" }

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

{ "code": "482915" }
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

{ "email_dispatched": true }

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).