/auth/*) used by workspace users, tenant
admins, and direct API integrations.
The token model
A successful login returns aTokenPair:
A short-lived JWT (
JWT_ACCESS_TTL, default 15 minutes). Send it as
Authorization: Bearer <accessToken> on every authenticated request. It embeds tenantId,
roleCode, and subjectScope as claims — there is no separate tenant header to set.A longer-lived JWT (
JWT_REFRESH_TTL, default 30 days) used to obtain a new access token.
Browser clients get this automatically as an httpOnly cookie scoped to /api/v1/auth; if
you’re integrating server-to-server, read it from the response body instead and store it
yourself.Access token lifetime, e.g.
"15m".Authorization: Bearer <accessToken>.
Register a tenant
2–80 characters.
Lowercase,
^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$. This becomes your login slug.2–80 characters.
Optional structured name (
givenName, familyName, nameOrder, phonetic fields for CJK
names, preferredDisplayName, nameLocaleRegion) — see the request example.Lowercased automatically.
8+ characters.
Initial subject company name.
Defaults to
free. Public registration only ever accepts free.Required when Turnstile is enabled.
AuthLoginResult (see me below for the user shape). Disabled in
production (PUBLIC_REGISTRATION_ENABLED=false) — use Redeem a code instead.
Public.
Redeem a code
Same response shape as register, plus acode field instead of planCode — the plan comes from
the code itself.
8–80 characters.
POST /auth/redeem · Public
Log in
6-digit TOTP code, if MFA is enrolled.
6-digit backup code from
/auth/login/mfa/email, used instead of mfaCode when the
authenticator is unavailable.Used during first-time MFA enrollment.
{ accessToken, refreshToken, expiresIn, tenant: { id, slug, name }, user } — see
session for user.{ mfaRequired: true, enrollmentRequired, message, secret?, otpauthUrl?, emailFallbackAvailable? }.
If enrollmentRequired is true, this is the account’s first login and it must enroll TOTP
before proceeding — see MFA.POST /auth/login · Public
Related:
POST /auth/login/resolve— given just anemail, a convenience lookup (no password) used by UIs to figure out which tenant(s) an email belongs to before showing the login form.POST /auth/login/mfa/email— re-verifiestenantSlug+email+password, then emails a 6-digit backup code to use asemailCodeon/auth/login.
Refresh and logout
Optional — falls back to the
alforse_tenant_refresh httpOnly cookie if omitted.POST /auth/refresh (Public) returns a new TokenPair. POST /auth/logout (requires
bearer token) revokes the refresh token and clears the cookie.
Session
{ id, email, name, nameProfile, status, roleCode, subjectScope }{ id, slug, name, edition }Module → permission level map.
Resolved plan feature flags.
Subject companies visible to this user.
{ timezone, reminderLeadDays, reminderHour, reminderEscalationDays, ownerScopeEnabled, preferences }GET /auth/me is the single call to hydrate a client session after login.
PATCH /auth/preferences— language, locale, timezone, currency, calendar/measurement system, first day of week, date/number format. All fields optional.PATCH /auth/profile—nameand/or a structurednameProfile.
MFA
| Endpoint | Body | Notes |
|---|---|---|
GET /auth/mfa/status | — | Whether TOTP is enrolled. |
POST /auth/mfa/enroll/start | — | Returns a new TOTP secret + otpauth URL to scan. |
POST /auth/mfa/enroll/confirm | { secret, code } | Confirms enrollment with a 6-digit code. |
POST /auth/mfa/disable | { code } | Requires a current valid 6-digit code. |
MFA_REQUIRED_FOR_ADMINS policy for themselves in production —
see Configuration.
Passkeys (WebAuthn)
| Endpoint | Body | Notes |
|---|---|---|
GET /auth/passkeys | — | List your registered passkeys. |
POST /auth/passkeys/register/options | — | Get WebAuthn registration options. |
POST /auth/passkeys/register/verify | { response, label? } | response is the browser’s RegistrationResponseJSON. |
DELETE /auth/passkeys/:id | — | Remove a passkey. |
POST /auth/passkeys/login/options | { email, tenantSlug, turnstileToken? } | Public. Start passkey login. |
POST /auth/passkeys/login/verify | { flowId, response } | Public. response is the browser’s AuthenticationResponseJSON. Returns a TokenPair like login. |
SSO
| Endpoint | Notes |
|---|---|
GET /auth/sso/providers | Public. Lists which of google / linkedin / dingtalk / lark / slack are configured. |
GET /auth/sso/:provider/start?tenantSlug=&next=&client= | Public. Redirects into the provider’s OAuth flow. |
GET /auth/sso/:provider/callback | Public. Provider redirects back here; Alforse redirects onward with a short-lived exchange code. |
POST /auth/sso/exchange | Public. { code } → a TokenPair (code must be 16+ characters, single use, ~2 minute TTL). |
Invitations and password reset
| Endpoint | Body | Notes |
|---|---|---|
POST /auth/invitations/accept | { token, tenantSlug?, name, nameProfile?, password, turnstileToken? } | Public. Accepts a member invitation and sets the account’s password. |
POST /auth/password-reset/request | { tenantSlug, email, turnstileToken? } | Public. Always returns { ok: true, deliveryStatus } — including when the tenant or email doesn’t exist (deliveryStatus: "not_sent") — to avoid account enumeration. |
POST /auth/password-reset/confirm | { token, tenantSlug, password, turnstileToken? } | Public. token is 16+ characters, single use, expires 2 hours after it was requested. |