Frontend API
HTTP endpoints the browser calls. The SDK wraps all of these.
You rarely call these yourself; the SDK does. They're documented so you can debug, or build a client in another language.
Every request needs:
x-publishable-key: pk_…header (or?pk=query).credentials: "include", so the refresh cookie is sent.- An
Originthat is the auth service itself or on the instance's allowed origins. Other origins get403on anything exceptGET. content-type: application/jsonfor bodies.
Sign up
POST /v1/client/sign_up
{ "email": "[email protected]", "password": "…", "firstName": "Ada", "lastName": "Lovelace", "legalAccepted": true }201 → { user, sessionId, jwt } and sets the refresh cookie.
Errors: email_taken (409), password_too_short (400), validation (422).
Sign in
POST /v1/client/sign_in
{ "email": "[email protected]", "password": "…" }200 → { status: "complete", user, sessionId, jwt } and sets the refresh cookie, or { status: "needs_second_factor" } with a 5-minute pending cookie (no session yet).
Errors: invalid_credentials (400, same for unknown email and wrong password), forbidden (403, banned), rate_limited (429).
Second factor
POST /v1/client/sign_in/second_factor { "code": "123456" } // or a backup codeNeeds the pending cookie from sign_in. 200 → { status: "complete", user, sessionId, jwt }. Five wrong codes void the attempt (401).
Single sign-on
POST /v1/client/sso/check { "email" } → { sso, connectionName, required }
GET /v1/client/sso/start?pk=…&email=…&redirect_url=… // navigate the browser here; it redirects to the company sign-in page
GET /v1/client/environment → { name, oauthProviders, passwordMinLength, legal, emailCodeSignIn, ssoEnabled, smsSecondFactor, ... }When required is true, every other sign-in method for that address answers 403 sso_required.
DELETE /v1/client/session { "redirectUrl"? } → { ok: true, ssoLogoutUrl? }Signs out. ssoLogoutUrl is only there when the session came from an SSO connection with single logout on: send the browser there to sign out at the identity provider too. It comes back to redirectUrl (an allowed origin) or the hosted sign-in page.
Two-factor setup (signed in)
POST /v1/client/mfa/totp → { secret, uri }
POST /v1/client/mfa/totp/verify { code } → { enabled: true, backupCodes: [...] }
POST /v1/client/mfa/totp/disable { code } → { enabled: false }
POST /v1/client/mfa/backup_codes { code } → { backupCodes: [...] }Turning 2FA off, making new backup codes, and adding or removing any factor need a current code (authenticator, texted or backup), or a security key touch made earlier in the same session (see below).
SMS
POST /v1/client/mfa/sms { phone, code? } → { phone } // texts a code; `code` needed if 2FA is already on
POST /v1/client/mfa/sms/verify { code } → { backupCodes }
POST /v1/client/mfa/sms/send → { phone } // text a code to confirm a change
POST /v1/client/mfa/sms/remove { code } → { removed: true }
POST /v1/client/sign_in/second_factor/sms // text a code during sign-inOnly numbers in the countries the instance allows can be added (sms_country_not_allowed).
Security keys
GET /v1/client/mfa/security_keys → { securityKeys: [{ id, name, createdAt, lastUsedAt }] }
POST /v1/client/mfa/security_keys/register/options { code? } → WebAuthn registration options
POST /v1/client/mfa/security_keys/register/verify { response, name? } → { id, backupCodes }
PATCH /v1/client/mfa/security_keys/{id} { name }
POST /v1/client/mfa/security_keys/{id}/remove { code? }
POST /v1/client/sign_in/second_factor/security_key → WebAuthn authentication options (needs the pending cookie)
POST /v1/client/sign_in/second_factor { response } // the key's answer; or { code, method? }Registering needs a sign-in from the last 15 minutes, and a current code if 2FA is already on. An account can hold 5 keys. A key is only ever a second factor; it can't sign anyone in alone.
Confirming with a key instead of a code. POST /v1/client/mfa/security_keys/confirm/options returns authentication options; POST /v1/client/mfa/security_keys/confirm { response } checks the answer and, for two minutes, lets that session make one sensitive change above by omitting code.
Account (signed in)
PATCH /v1/client/me { "firstName": "Grace", "lastName": null } → { user }
GET /v1/client/me/security → { hasPassword, twoFactorEnabled, totpEnabled, smsPhone, smsAvailable, securityKeys, backupCodesRemaining, connectedAccounts }
POST /v1/client/password/change { "currentPassword", "newPassword" } → { changed, otherSessionsSignedOut }
GET /v1/client/sessions → { sessions: [{ id, current, device, ipHint, lastActiveAt, createdAt }] }
POST /v1/client/sessions/{id}/revoke
POST /v1/client/sessions/revoke_others → { signedOut }A session ID that isn't yours returns 404, the same as one that doesn't exist, so IDs can't be probed.
Passkeys
POST /v1/client/passkeys/register/options → PublicKeyCredentialCreationOptions (needs a sign-in < 15 min old)
POST /v1/client/passkeys/register/verify { response, name? } → { id }
POST /v1/client/passkeys/authenticate/options → PublicKeyCredentialRequestOptions (no session needed)
POST /v1/client/passkeys/authenticate/verify { response } → { status: "complete", user, sessionId, jwt }
GET /v1/client/passkeys → { passkeys: [...] }
PATCH /v1/client/passkeys/{id} { name }
DELETE /v1/client/passkeys/{id}response is the JSON-serialised PublicKeyCredential from the browser; the SDK produces it for you.
Get a fresh token
POST /v1/client/session/token200 → { jwt, sessionId, userId }. 401 when the session has ended.
Current session
GET /v1/client/session200 → { session: { id, expiresAt }, user } or { session: null, user: null }.
Sign out
DELETE /v1/client/session200 → { ok: true } and clears the cookie.
Email verification (signed in)
POST /v1/client/email/send_code
POST /v1/client/email/verify { "code": "123456" }Password reset
POST /v1/client/password/forgot { "email": "[email protected]" }
POST /v1/client/password/reset { "email": "…", "code": "123456", "password": "…" }forgot always returns 200 { sent: true }. reset returns 200 { reset: true }, revokes all the user's sessions and clears the cookie.
Instance settings
GET /v1/client/environment200 → { name, oauthProviders: ["google", "github"], passwordMinLength, legal: { required, termsUrl, privacyUrl, version } }
Social sign-in
GET /v1/client/oauth/{google|github}/start?pk=pk_…&redirect_url=…&legal_accepted=1Open this as a full-page navigation, not fetch. It redirects to the provider and sets a short-lived state cookie. The provider returns to GET /v1/oauth/callback/{provider}, which signs the user in and redirects to redirect_url (if allowed) or to the hosted page. Errors go to /sign-in?error=<code>; see Social sign-in.
Public keys
GET /i/{instanceId}/.well-known/jwks.json
GET /i/{instanceId}/.well-known/openid-configurationBoth are public, cacheable for 5 minutes, and allow any origin.
Rate limits
| Endpoint | Limit |
|---|---|
| sign_in | 20 / 5 min per IP and 10 / 15 min per email |
| sign_up | 10 / hour per IP |
| session/token | 120 / min per IP |
| email/send_code | 5 / hour per user |
| email/verify | 20 / 15 min per IP |
| password/forgot | 10 / hour per IP, 5 / hour per email (silently) |
| password/reset | 20 / 15 min per IP |
| sign_in/second_factor | 30 / 15 min per IP, and 5 wrong codes per attempt |
| mfa/* | 10–20 / 15 min per IP |
| passkeys/authenticate/* | 60 options, 30 verifications / 5 min per IP |
| passkeys/register/options | 20 / 15 min per IP |
Organizations
All need a signed-in session unless noted.
| Endpoint | Description |
|---|---|
GET / POST /v1/client/organizations |
List mine / create |
PATCH / DELETE /v1/client/organizations/{orgId} |
Rename / delete (admin) |
GET /v1/client/organizations/{orgId}/members |
List members |
PATCH / DELETE /v1/client/organizations/{orgId}/members/{userId} |
Change role / remove (me to leave) |
GET / POST /v1/client/organizations/{orgId}/invitations |
List / invite (admin) |
DELETE /v1/client/organizations/{orgId}/invitations/{id} |
Revoke (admin) |
POST /v1/client/invitations/preview |
Org name and role for a token (works signed out) |
POST /v1/client/invitations/accept |
Join with a token |
POST /v1/client/session/active_org |
Switch active organization; returns a new JWT |