Mechanism

Bearer tokens — not cookie sessions. The web and mobile clients obtain a token by logging in; third-party scripts and integrations use a user-bound API key instead (see API Access), which is the same bearer mechanism with a read or read & write scope. There is no OAuth client-credentials flow.

Login endpoint

POST /api/v1/auth/login — email/password. Two outcomes:

  • Success (no 2FA): returns a a bearer token immediately.
  • 2FA enrolled: returns a challenge response instead; the client must then re-submit with a totp_code (or recovery code) to complete login and receive the token.

Google SSO is an alternate path to the same result — see Integration — Google SSO: the client ends up calling POST /auth/google/exchange to trade a short-lived exchange code for the same kind of bearer token (also TOTP-gated if enrolled).

Using the token

Every authenticated request must send:

Authorization: Bearer <token>

and, for tenant-scoped endpoints:

X-Tenant: <tenant-uuid>

The X-Tenant value is only trusted if it matches one of the caller's own active memberships — a client cannot access another tenant's data by simply changing this header.

Token expiration

Default token lifetime is 7 days, after which the token stops working even without explicit revocation.

Token refresh

There is no refresh-token endpoint — the client re-authenticates (login or SSO) to obtain a new token when the old one expires or is revoked.

Logout / session management

  • POST /auth/logout — revokes the current token.
  • GET /profile/sessions — lists the caller's active tokens.
  • DELETE /profile/sessions/{tokenId} — revokes a specific token (e.g. remote sign-out of another device).

MFA (TOTP)

  • POST /auth/2fa/enroll → POST /auth/2fa/confirm — two-step enrollment; a newly generated secret is staged as "pending" and only activated on confirm, so a bare enroll call can never silently disable existing 2FA.
  • POST /auth/2fa/disable — requires re-verification (current password, or an emailed OTP for SSO-only accounts with no password).
  • POST /auth/2fa/recovery-codes/regenerate — invalidates old recovery codes and issues new ones.
  • Independent of login-time MFA, some sensitive actions (approval, posting, reversal, bank reconciliation completion) can require a fresh TOTP code again at the point of action, per tenant policy flags (require_totp_for_review/_approval/_posting, all default true). See Authentication & Authorization.

Permission enforcement errors

  • No X-Tenant header, or it doesn't match an active membership → 409.
  • Valid tenant but missing the route's required permission:<key> → 403 with the required permission key in the response body.
  • No active subscription (tenant billing lapsed) → 402 on write requests (reads and */billing/* remain available).
  • Plan limit reached → 402 plan_limit_reached (user seats) or 402 usage_limit_reached (monthly transaction cap, returned by the approve/post step).
  • Tenant's country doesn't match a country:<ISO2> gate → 404 (not a 403 — the route is presented as not existing for that tenant).

See API Error Reference for the full status-code catalogue.

Rate limiting on auth endpoints

Login 20/min, register 10/min, password-forgot 6/min, password-reset 10/min, email-verification-related 6/min, Google OAuth redirect/callback 20/min, 2FA enroll/confirm/disable/recovery-regenerate 10/min each. TOTP verification is additionally capped at 5 attempts/minute independent of the route throttle (tracked in totp_attempts). See Security.