Sign-in
Registration, sign-in, two-factor and the hand-off between the site and a workspace.
- POST/v1/auth/2fa/setup
- POST/v1/auth/2fa/setup/confirm
- POST/v1/auth/2fa/verify
- POST/v1/auth/account-handoff
- POST/v1/auth/account-handoff/exchange
- POST/v1/auth/account-login
- POST/v1/auth/email/resend
- POST/v1/auth/email/verify
- POST/v1/auth/google
- POST/v1/auth/handoff
- POST/v1/auth/handoff/exchange
- POST/v1/auth/invite/accept
- POST/v1/auth/login
- POST/v1/auth/logout
- POST/v1/auth/password-reset/confirm
- POST/v1/auth/password-reset/request
- POST/v1/auth/refresh
- POST/v1/auth/register
/v1/auth/2fa/setupStart mandatory 2FA enrolment during sign-in
For an account that answered twoFactorSetupRequired. Redeems the challengeToken for a fresh secret + QR code. Nothing is live until /auth/2fa/setup/confirm.
Needs: Nothing. This route is public.
| Field | Type | What it is |
|---|---|---|
| challengeToken required | string | From the twoFactorSetupRequired response. |
| Status | Body | When |
|---|---|---|
| 200 | TwoFactorSetupResponseDto | |
| 401 | — | Invalid or expired challenge |
| Field | Type | What it is |
|---|---|---|
| secret required | string | For manual entry, when the authenticator app can't scan a code. |
| otpauthUrl required | string | |
| qrCodeDataUrl required | string | A data:image/png;base64,... QR code of otpauthUrl — draw it straight into an <img>. |
/v1/auth/2fa/setup/confirmFinish mandatory 2FA enrolment and sign in
Proves the authenticator app has the secret, turns 2FA on, and returns the session the original register/login call would have — plus backupCodes, which are shown once and never again.
Needs: Nothing. This route is public.
| Field | Type | What it is |
|---|---|---|
| challengeToken required | string | From the twoFactorRequired response. |
| code required | string | A 6-digit authenticator code, or one of the 10-character backup codes. |
| Status | Body | When |
|---|---|---|
| 200 | (AccountSessionResponseDto | AuthSessionResponseDto) & object | The session (account token, or a full workspace session when the sign-in named one) with backupCodes: string[] merged in. |
| 401 | — | Invalid/expired challenge, or a wrong code |
| Field | Type | What it is |
|---|---|---|
| accountToken required | string | Identity only, no org. Use it for /v1/billing/demo and /v1/billing/checkout. |
| user required | UserSummaryDto | |
| user.id required | string | |
| user.email required | string | |
| backupCodes required | string[] |
| Field | Type | What it is |
|---|---|---|
| accessToken required | string | |
| refreshToken required | string | |
| user required | UserSummaryDto | |
| user.id required | string | |
| user.email required | string | |
| organization required | OrganizationSummaryDto | The active organization: the newly-created org on register, or the workspace signed in to on login (the subdomain's org). Always the org the token is scoped to. |
| organization.id required | string | |
| organization.name required | string | |
| organization.slug required | string | |
| backupCodes required | string[] |
/v1/auth/2fa/verifyComplete a 2FA-gated sign-in
Redeems the challengeToken from account-login/login/google plus a TOTP or backup code, and returns exactly what that original call would have.
Needs: Nothing. This route is public.
| Field | Type | What it is |
|---|---|---|
| challengeToken required | string | From the twoFactorRequired response. |
| code required | string | A 6-digit authenticator code, or one of the 10-character backup codes. |
| Status | Body | When |
|---|---|---|
| 200 | AccountSessionResponseDto | AuthSessionResponseDto | |
| 401 | — | Invalid/expired challenge, or invalid/expired code |
| Field | Type | What it is |
|---|---|---|
| accountToken required | string | Identity only, no org. Use it for /v1/billing/demo and /v1/billing/checkout. |
| user required | UserSummaryDto | |
| user.id required | string | |
| user.email required | string |
| Field | Type | What it is |
|---|---|---|
| accessToken required | string | |
| refreshToken required | string | |
| user required | UserSummaryDto | |
| user.id required | string | |
| user.email required | string | |
| organization required | OrganizationSummaryDto | The active organization: the newly-created org on register, or the workspace signed in to on login (the subdomain's org). Always the org the token is scoped to. |
| organization.id required | string | |
| organization.name required | string | |
| organization.slug required | string |
/v1/auth/account-handoffCreate a one-time code for going from a workspace to the customer area
The reverse of POST /v1/auth/handoff. Billing lives on the marketing origin, not in the panel, and a session cookie cannot cross between the two — so someone clicking "choose a plan" in the panel carries a short-lived single-use code instead of retyping their password. The workspace comes from the caller’s own token, never from the body.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Status | Body | When |
|---|---|---|
| 200 | HandoffDto |
| Field | Type | What it is |
|---|---|---|
| code required | string | Single-use, expires in about a minute. |
| url required | string | Where to redeem it — the workspace's own origin. |
/v1/auth/account-handoff/exchangeRedeem a one-time code for an account session
Yields identity only — the same token account-login returns. Arriving from a workspace never produces a stronger credential than signing in here would.
Needs: Nothing. This route is public.
| Field | Type | What it is |
|---|---|---|
| code required | string | The one-time code from the sign-in host. |
| Status | Body | When |
|---|---|---|
| 200 | AccountHandoffSessionDto |
| Field | Type | What it is |
|---|---|---|
| accountToken required | string | |
| user required | UserSummaryDto | |
| user.id required | string | |
| user.email required | string | |
| organization required | OrganizationSummaryDto | null | |
| organization.id required | string | |
| organization.name required | string | |
| organization.slug required | string |
/v1/auth/account-loginVerify a password without entering a workspace
Used by the shared sign-in host and the customer area, where nobody has chosen a workspace yet. Always answers with a two-factor step, never a token directly.
Needs: Nothing. This route is public.
| Field | Type | What it is |
|---|---|---|
| email required | string (email) | The account email. |
| password required | string |
| Status | Body | When |
|---|---|---|
| 200 | TwoFactorChallengeResponseDto | TwoFactorSetupRequiredResponseDto | Every account must have 2FA on — there is no opt out. An account that already has it enrolled gets a twoFactorRequired challenge (redeem at POST /auth/2fa/verify); one that does not gets twoFactorSetupRequired instead (redeem at POST /auth/2fa/setup, then /auth/2fa/setup/confirm). Neither is optional to handle: no account can reach a real session without going through one of them. |
| Field | Type | What it is |
|---|---|---|
| twoFactorRequired required | boolean | |
| challengeToken required | string | Trade this, plus a code, at POST /v1/auth/2fa/verify. Expires in ~10 minutes. |
| Field | Type | What it is |
|---|---|---|
| twoFactorSetupRequired required | boolean | |
| challengeToken required | string |
/v1/auth/email/resendSend another confirmation email
ALWAYS returns 204, for the same enumeration reason as the reset request. An unknown address, an already-confirmed one and a pending one are indistinguishable from the response.
Needs: Nothing. This route is public.
| Field | Type | What it is |
|---|---|---|
| email required | string (email) | The account email. Always answered with 204, like the reset request. |
| Status | Body | When |
|---|---|---|
| 204 | — | Accepted. Says nothing about whether the address exists. |
/v1/auth/email/verifyConfirm an email address
Marks the address as confirmed, using the token from the emailed link.
Needs: Nothing. This route is public.
| Field | Type | What it is |
|---|---|---|
| token required | string | The token from the emailed link. |
| Status | Body | When |
|---|---|---|
| 200 | EmailVerifiedResponseDto | |
| 400 | — | The link is invalid, expired, or already used. |
| Field | Type | What it is |
|---|---|---|
| email required | string | |
| verifiedAt required | string | ISO timestamp of when the address was first confirmed. |
/v1/auth/googleSign in with Google
Takes the ID token from Google Identity Services and returns an ACCOUNT token — identity, no workspace — exactly as register does. A Google identity says who you are, not which workspace you meant.
Needs: Nothing. This route is public.
| Field | Type | What it is |
|---|---|---|
| idToken required | string | The ID token returned by Google Identity Services. Verified server-side against Google's published keys — a decoded token proves nothing. |
| allowSignup | boolean | May an unknown Google address create an account? True on the marketing site, where Google is a sign-up route. The panel sign-in host sends false, so an address with no account is told so instead of silently getting one. |
| orgSlug | string | Name a workspace to receive a full session for it, as the panel does when signing in on a workspace subdomain. Omit it — as the marketing site does — to receive an account token with no workspace. |
| Status | Body | When |
|---|---|---|
| 200 | AuthSessionResponseDto | AccountSessionResponseDto | A full session when orgSlug was given, otherwise an account token. Google sign-in is never itself gated behind this account's 2FA — see AuthService. |
| 401 | — | The Google token could not be verified |
| Field | Type | What it is |
|---|---|---|
| accessToken required | string | |
| refreshToken required | string | |
| user required | UserSummaryDto | |
| user.id required | string | |
| user.email required | string | |
| organization required | OrganizationSummaryDto | The active organization: the newly-created org on register, or the workspace signed in to on login (the subdomain's org). Always the org the token is scoped to. |
| organization.id required | string | |
| organization.name required | string | |
| organization.slug required | string |
| Field | Type | What it is |
|---|---|---|
| accountToken required | string | Identity only, no org. Use it for /v1/billing/demo and /v1/billing/checkout. |
| user required | UserSummaryDto | |
| user.id required | string | |
| user.email required | string |
/v1/auth/handoffCreate a one-time code for entering a workspace
The session belongs to the workspace origin, and a cookie cannot cross between two sites — so a short-lived single-use code travels in the URL instead. It is worth nothing after about a minute, or after one use.
Needs: A signed-in panel session, sent as Authorization: Bearer <access token>.
| Field | Type | What it is |
|---|---|---|
| orgSlug required | string | The workspace being entered. |
| Status | Body | When |
|---|---|---|
| 200 | HandoffDto |
| Field | Type | What it is |
|---|---|---|
| code required | string | Single-use, expires in about a minute. |
| url required | string | Where to redeem it — the workspace's own origin. |
/v1/auth/handoff/exchangeRedeem a one-time code for a session
Checks access again rather than trusting the code: a code created a minute ago cannot know whether the workspace was suspended since.
Needs: Nothing. This route is public.
| Field | Type | What it is |
|---|---|---|
| code required | string | The one-time code from the sign-in host. |
| Status | Body | When |
|---|---|---|
| 200 | AuthSessionResponseDto |
| Field | Type | What it is |
|---|---|---|
| accessToken required | string | |
| refreshToken required | string | |
| user required | UserSummaryDto | |
| user.id required | string | |
| user.email required | string | |
| organization required | OrganizationSummaryDto | The active organization: the newly-created org on register, or the workspace signed in to on login (the subdomain's org). Always the org the token is scoped to. |
| organization.id required | string | |
| organization.name required | string | |
| organization.slug required | string |
/v1/auth/invite/acceptRedeem a team invitation
An email that already has an account joins directly. An email with none needs name and password to create one; without them the answer is a 400, which a client can show as a "create your account" step before sending again.
Needs: Nothing. This route is public.
| Field | Type | What it is |
|---|---|---|
| token required | string | The <id>.<secret> token from the invite link. |
| name | string | Only used when accepting the invite creates a brand-new account — an invite for an email that already has one ignores both fields entirely. |
| password | string | Required only when no account exists yet for the invited email. |
| Status | Body | When |
|---|---|---|
| 200 | TwoFactorChallengeResponseDto | TwoFactorSetupRequiredResponseDto | The invitation is claimed either way — but the session behind it still comes through the two-factor step. Every account must have 2FA on — there is no opt out. An account that already has it enrolled gets a twoFactorRequired challenge (redeem at POST /auth/2fa/verify); one that does not gets twoFactorSetupRequired instead (redeem at POST /auth/2fa/setup, then /auth/2fa/setup/confirm). Neither is optional to handle: no account can reach a real session without going through one of them. |
| Field | Type | What it is |
|---|---|---|
| twoFactorRequired required | boolean | |
| challengeToken required | string | Trade this, plus a code, at POST /v1/auth/2fa/verify. Expires in ~10 minutes. |
| Field | Type | What it is |
|---|---|---|
| twoFactorSetupRequired required | boolean | |
| challengeToken required | string |
/v1/auth/loginAuthenticate with email + password against one workspace
Always answers with a two-factor step, never a session directly.
Needs: Nothing. This route is public.
| Field | Type | What it is |
|---|---|---|
| email required | string (email) | |
| password required | string | |
| orgSlug required | string | The workspace subdomain slug to sign in to (server resolves org + role). |
| Status | Body | When |
|---|---|---|
| 200 | TwoFactorChallengeResponseDto | TwoFactorSetupRequiredResponseDto | Every account must have 2FA on — there is no opt out. An account that already has it enrolled gets a twoFactorRequired challenge (redeem at POST /auth/2fa/verify); one that does not gets twoFactorSetupRequired instead (redeem at POST /auth/2fa/setup, then /auth/2fa/setup/confirm). Neither is optional to handle: no account can reach a real session without going through one of them. |
| 401 | — | Invalid email or password |
| Field | Type | What it is |
|---|---|---|
| twoFactorRequired required | boolean | |
| challengeToken required | string | Trade this, plus a code, at POST /v1/auth/2fa/verify. Expires in ~10 minutes. |
| Field | Type | What it is |
|---|---|---|
| twoFactorSetupRequired required | boolean | |
| challengeToken required | string |
/v1/auth/logoutRevoke a refresh token
Needs: Nothing. This route is public.
| Field | Type | What it is |
|---|---|---|
| refreshToken required | string |
| Status | Body | When |
|---|---|---|
| 204 | — |
/v1/auth/password-reset/confirmSet a new password using a reset link
Single-use and short-lived. On success the new password is stored, EVERY refresh token the user holds is revoked, and a "your password was changed" notice is sent. Expired, already-used and unknown tokens all return the same 400 with the same message.
Needs: Nothing. This route is public.
| Field | Type | What it is |
|---|---|---|
| token required | string | The token from the emailed link. |
| password required | string | The new password: 8 to 128 characters, the same rule as at sign-up. |
| Status | Body | When |
|---|---|---|
| 204 | — | Password changed; all sessions revoked. |
| 400 | — | The link is invalid, expired, or already used. |
/v1/auth/password-reset/requestAsk for a password-reset link
ALWAYS returns 204, whether or not the address has an account. Any other behaviour — a 404, a different body, a different error — would let anyone test an email list against our user table. The client must therefore show "if that address has an account, we have sent a link" and nothing more specific.
Needs: Nothing. This route is public.
| Field | Type | What it is |
|---|---|---|
| email required | string (email) | The account email. The response is 204 whether or not it exists — see the endpoint description. |
| Status | Body | When |
|---|---|---|
| 204 | — | Accepted. Says nothing about whether the address exists. |
/v1/auth/refreshRotate a refresh token for a new access + refresh pair
Needs: Nothing. This route is public.
| Field | Type | What it is |
|---|---|---|
| refreshToken required | string |
| Status | Body | When |
|---|---|---|
| 200 | RefreshResponseDto | |
| 401 | — | Invalid, expired, or reused refresh token |
| Field | Type | What it is |
|---|---|---|
| accessToken required | string | |
| refreshToken required | string |
/v1/auth/registerCreate an account
Creates the user only — no workspace. A workspace costs either a demo request (POST /v1/billing/demo) or a payment (POST /v1/billing/checkout); the returned account token authenticates both.
Needs: Nothing. This route is public.
| Field | Type | What it is |
|---|---|---|
| email required | string (email) | |
| password required | string | The password: 8 to 128 characters. |
| name | string | The person's own name. Optional, but when given it must be at least two characters and not only spaces. |
| organizationName | string | The workspace's name. Optional: a name is made from the email address when it is left out. When given it must not be only spaces. |
| Status | Body | When |
|---|---|---|
| 201 | TwoFactorSetupRequiredResponseDto | Never a token: a brand-new account has no 2FA yet and enrolment is mandatory, so registration always answers with twoFactorSetupRequired. The account token arrives from POST /auth/2fa/setup/confirm once enrolment finishes. |
| 409 | — | Email already registered |
| Field | Type | What it is |
|---|---|---|
| twoFactorSetupRequired required | boolean | |
| challengeToken required | string |
Next groupAccount