Examples
Every auth request sends your tenant identity in the headers. The body carries only data (email, password, code) — never ids or secrets.
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 one-time code
curl -X POST https://auth.example.com/api/v1/auth/login/verify-otp \
-H "X-Tenant-Id: atr_xxxxxxxxxxxx" \
-H "Authorization: Bearer alcs_xxxxxxxx..." \
-H "Content-Type: application/json" \
-d '{"email":"user@acme.com","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 preflight (or verify-otp) hands you 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 }idis 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 Bearersecret doesn't match that tenant. Confirm both from the Tenant Credentials card (rotate to get a fresh secret if unsure).