Examples
Every auth request sends your tenant identity in the headers. The body carries only data (email, password, code) — never ids or secrets.
API sign-in v2 — password, proof, setup, exchange
Authenticate every call from your backend with the app login credential./api/v1/auth/login/preflight serves only v2; the optional Authreads-Login-Flow header accepts only 2. After password verification, follow next_method and alternatives: email code, authenticator, recovery code or a passkey on a verified application domain. Replace your stored flow_token after every transition. The original ten-minute expiry never extends; Back cancels the flow. Keep secrets and capabilities out of browser JavaScript, URLs and logs.
POST /api/v1/auth/login/preflight
Authreads-Login-Flow: 2
{"email":"user@example.com","password":"…","min_acr":"urn:authreads:acr:mfa"}
POST /api/v1/auth/login/flow/verify-totp
{"flow_token":"<latest capability>","code":"<authenticator code>"}
# Only after a response containing grant:
POST /api/v1/auth/session/exchange
{"grant":"<one-time grant>"}An unfinished response omits grant. Handle password_change_requiredwith /api/v1/auth/password/change-expired and {flow_token,new_password}, then restart sign-in. For strong_method.state Ask or Required, use/login/flow/enrol/totp/start and /confirm, or the passkey enrollment begin/finish endpoints. Only unexpired Ask permits /login/flow/remind-later. Enrollment authority is the flow; no account session is issued before setup. Setup uses the same account-wide strongest-factor rule as account security, including passkeys on another RP. Select an existing authenticator or recovery code before setup when that passkey cannot be used here. Preflight prefers an available recovery code before sending an email that cannot authorize setup; always follow the returned method. A final consumed recovery code remains repair proof; it does not become MFA evidence.
For passkeys, send the exact registered Authreads-Passkey-Origin on preflight, then call /login/flow/passkey/begin with the latest flow and origin. Forward the returned challenge options to the browser or native client and return the signed credential with its ceremony ID to /finish. Android certificate origins and Apple associated domains require verified app-domain registration; an arbitrary display domain is insufficient.
flow_superseded (409) requires a new flow; retries never issue another grant. Reverify returns 401 session_invalid for unknown, other-tenant, revoked or expired sessions. Clear the application session and require sign-in. A live same-tenant session for another app returns 403 session_app_mismatch. Respect Retry-After on 429. login_proof_rejected andlogin_method_unavailable need a server-directed proof or method change.strong_method_setup_required blocks completion until setup.fresh_authentication_required requires a fresh strongest-method sign-in; additional methods belong in authenticated account security.stronger_proof_unavailable_here means no usable repair proof is available: manage methods where the existing passkey works or use the organisation’s audited reset process.recovery_proof_required (403 on setup) means only a recovery code can authorize setup on this site: restart sign-in and choose recovery_code, or manage methods where the passkey works. A response carrying recovery_codes_exhausted: true (a sign-in with the last recovery code, or a setup it authorised) left the account with no unspent recovery codes; prompt the user to generate new ones. A malformed remembered-browser value returns 400trusted_device_invalid: discard the stored value and retry without it.app_passkey_domain_unverified and app_login_unavailable need an administrator repair. Retired endpoints return endpoint_removed with HTTP 410.Completed v2 grants and valid end-user session responses expose acr,auth_time and cumulative amr. Contexts are orderedsingle, pwd, 2fa, mfa,phr, each prefixed with urn:authreads:acr:. Email and recovery with password reach 2fa; authenticator with password reaches mfa; a user-verified passkey reaches phr. Trusted-browser skips and enrollment bootstrap stay pwd. The proof supporting the strongest context owns auth_time; later password checks cannot make an older strong proof appear fresh. Operator tokens omit this end-user context. These values do not assert NIST AAL certification.
Request an optional min_acr on v2 preflight to restrict the proof methods. An existing password-proven app session can instead POST/api/v1/auth/login/flow/reverify with{"session_id":"<session bearer>","min_acr":"urn:authreads:acr:mfa"}. Follow the same rotating proof flow. Completion returns session_reverified:trueand fresh context without a grant; retain the original session and expiry. Passkeys require the exact verified origin at admission and ceremony calls.unmet_authentication_requirements means no configured method meets the floor; direct the user to account security setup. Trusted-device skips cannot satisfy it.
remember_device_unavailable is a 400 refusal before proof consumption; clear the option and retry with remember_device=false. Hosted OAuth publishes the same value in error_code. API and hosted offers, acceptance and token creation share one rule; reverify, Required rollout, recovery and requests above password strength cannot mint browser trust. Refresh the offer after method changes and resend.
On an actionable verification step, trusted_device_available tells you whether to offer remember-device; trusted_device_window_days uses effective policy. These fields are omitted during setup, password change, reverify and terminal responses. Test amr membership, never equality. First strong-factor enrolment returns recovery_codes once. Follow Let users manage 2FA from your app for authenticated account changes and lost-response recovery.
For a shared browser, keep a bounded, newest-first device_tokens list in backend-owned storage or an HttpOnly cookie. Send it on preflight and again with a completed verification. After completion, replace the stored list with the returneddevice_tokens. Keep the old material through a code challenge so a verified re-trust can rotate the same browser record. The platform permits at most ten entries, with a default storage cap of five. Never send both input forms; the singulardevice_token remains supported for existing clients. Keep all device credentials out of browser JSON and logs. Each entry is checked only for the user proving their password, tenant, pool, browser fingerprint and current strength requirement.
Tenant administrators can review the latest invitation, sign-in code and confirmation in Admin’s Email delivery section; platform operators have the equivalent Console view. Status is limited to the configured retention window and current address generation. Delivered means the receiving mail server accepted the message, not that it was read. These are private management views, not additional public sign-in endpoints. A current-address permanent bounce or complaint blocks invitation resends withinvite_delivery_blocked. A committed provisioning result instead carriesinvite.reason: delivery_blocked; preserve the successful account change and show its delivery warning. Correct the address or investigate the complaint before retrying.
alcs_… app login secret or returned session bearer in browser JavaScript. Replace atr_…, alcs_… and the host with your own values. The two headers are the same on every call:X-Tenant-Id: atr_xxxxxxxxxxxx
Authorization: Bearer alcs_xxxxxxxx...Start a login (preflight)
curl -X POST https://auth.example.com/api/v1/auth/login/preflight \
-H "X-Tenant-Id: atr_xxxxxxxxxxxx" \
-H "Authorization: Bearer alcs_xxxxxxxx..." \
-H "Content-Type: application/json" \
-d '{"email":"user@acme.com","password":"••••••••"}'Verify the offered email code
Use this endpoint only when next_method is email_code. Otherwise follow the factor and reverify guide and the method-specific flow. Enrollment can return recovery codes once; require the user to save them.
curl -X POST https://auth.example.com/api/v1/auth/login/flow/verify-email-code \
-H "X-Tenant-Id: atr_xxxxxxxxxxxx" \
-H "Authorization: Bearer alcs_xxxxxxxx..." \
-H "Content-Type: application/json" \
-d '{"flow_token":"<latest flow token>","code":"123456"}'Request a password reset
curl -X POST https://auth.example.com/api/v1/auth/password/forgot \
-H "X-Tenant-Id: atr_xxxxxxxxxxxx" \
-H "Authorization: Bearer alcs_xxxxxxxx..." \
-H "Content-Type: application/json" \
-d '{"email":"user@acme.com"}'Exchange a grant for a session
After the selected proof flow returns a one-time grant, trade it for a durable session. Returns { user, session_id, account_session_id, session_policy, session_activity_token, kind } — store all of it server-side, then mint an access token from session_id. Full sequence, and the idle-timeout contract carried by session_policy / session_activity_token: Sessions & tokens.
curl -X POST https://auth.example.com/api/v1/auth/session/exchange \
-H "X-Tenant-Id: atr_xxxxxxxxxxxx" \
-H "Authorization: Bearer alcs_xxxxxxxx..." \
-H "Content-Type: application/json" \
-d '{"grant":"<one-time-grant>"}'
# → {"user": {"id": "...", "email": "user@acme.com"}, "session_id": "5b1e...",
# "account_session_id": "9c2a...", "session_policy": {"idle_timeout_minutes": 30, ...},
# "session_activity_token": "...", "kind": null}Create a user (Management API)
Get a machine token, then create the user in a tenant org with a role:
curl -X POST https://auth.example.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=...&client_secret=..."
# → { "access_token": "...", "token_type": "Bearer", "expires_in": 900, "scope": "users:write ..." }curl -X POST https://auth.example.com/api/v1/management/users \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"email":"user@acme.com","organization_id":"<org uuid>","role_id":"<role uuid>"}'
# → { "id": "<user id>", "email": "...", "email_verified": false,
# "set_password": { "link": "..." } | null }id is the user's Authreads id — you don't copy it anywhere; your product resolves the user automatically on their first login. Omit password to get a set-password link instead. Full field reference: see the schema for CreateUserRequest in the API Reference.Email verification
New users are unverified unless you explicitly assert evidence. Omit email_verified during creation to assert nothing. Authreads automatically records first-hand proof when the user redeems a login or step-up code, invite/onboarding link, or reset link delivered by Authreads email. A raw set-password link returned through this API does not verify the address.
curl -X PATCH \
https://auth.example.com/api/v1/management/users/<user-id>/email-verification \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"email_verified":true}'
# → { "id":"…", "email_verified":true,
# "email_verified_method":"tenant_asserted", "email_verified_at":"…" }false request revokes only your own tenant_asserted evidence; it cannot downgrade authreads_otp or authreads_token proof. Every transition is tenant-scoped and durably audited.401 almost always means the X-Tenant-Id is wrong, or the Bearer secret doesn't match that tenant. Confirm both from the Tenant Credentials card (rotate to get a fresh secret if unsure).