Authentication flow
Two header patterns. Pick the one that matches the call you're making.
Anything done as a logged-in user. JWTs are HS256 (default) or RS256 (dual-signed when configured).
Authorization: Bearer <access_token>
Server-to-server. Example: a partner site posting /auth/login on behalf of a user.
X-OTL-Site-Key: otr_<...>
End-to-end flow
- Your server calls
POST /api/v1/auth/loginwithX-OTL-Site-Keyand the user's credentials. - OneTimeLogin returns
access_token(JWT, 30 min),refresh_token(opaque, 30 days), and theuserobject. - Send
access_tokenasAuthorization: Bearer <token>on every authenticated call. - When you get a
401, exchange the refresh token atPOST /api/v1/auth/refresh— you receive a fresh access token and a rotated refresh token. The old refresh token is immediately revoked (one-time-use rotation, replay-protected). - To force-logout a user everywhere, call
POST /api/v1/auth/logoutorDELETE /api/v1/sessions/{id}. This bumps the user's session_version and every existing access token becomes invalid on next call.
Token storage
- Access tokens: store in
sessionStorageor memory. NeverlocalStorage. - Refresh tokens: store in HttpOnly Secure SameSite=Strict cookies, server-side only.
- Site Keys: server-side secret manager. Never ship to a browser.
- Transport: HTTPS only in production. Localhost is allowed in development.
Working code in the 15-minute quickstart. SAML / OIDC enterprise SSO available on the Business and Enterprise tiers — see Enterprise SSO.
Resources
Eight resource groups. Each card lists the most-used endpoints.
Auth
View →register, login, logout, refresh, MFA verify
Users
View →list, create, update, delete
Sites
View →list user's sites, register a new site
Roles
View →list role catalog, assign role
Sessions
View →list, revoke
Webhooks
View →register endpoint, list events, replay
Bulk Registration
View →CSV upload, status check
Payments
View →list orders, retrieve order
Auth
All auth endpoints require X-OTL-Site-Key — they create or mutate user sessions for one site.
/api/v1/auth/registerCreate a new user/api/v1/auth/loginIssue access + refresh tokens/api/v1/auth/logoutRevoke current session/api/v1/auth/refreshRotate refresh token, mint new access token/api/v1/auth/mfa/verifySubmit MFA code to complete loginUsers
Manage users registered on your site. Bearer auth.
/api/v1/usersList users (paginated)/api/v1/users/{user_id}Retrieve one user/api/v1/usersCreate a user/api/v1/users/{user_id}Update user fields/api/v1/users/{user_id}Delete a userSites
A user can belong to many partner sites. Site-level register requires Bearer auth.
/api/v1/sitesList the current user's sites/api/v1/sitesRegister a new site & receive its site_keyRoles
Roles are scoped to one site. The catalog is read-only; assignment requires admin Bearer.
/api/v1/rolesList role catalog/api/v1/users/{user_id}/rolesAssign a role to a userSessions
List active sessions and revoke them. Revocation bumps the user's session version — every existing access token for that user is invalid on next call.
/api/v1/sessionsList the current user's sessions/api/v1/sessions/{session_id}Revoke a specific sessionWebhook flow
How delivery works
- You register a
webhook_urland receive awebhook_secreton your site. - When an event fires, we
POSTa JSON body to that URL. - Each request carries
X-OTL-Signature: sha256=<hex>where the hex isHMAC-SHA256(body, webhook_secret), andX-OTL-Event-Idfor idempotency. - Verify the signature with
hmac.compare_digest()(or your language's constant-time equivalent) before parsing the body. - Respond
2xxwithin 5 seconds. Anything else — or a timeout — triggers a retry.
Retry / backoff
Up to 3 attempts for back-channel events (SLO, deletion) with delays of 1 s, 5 s, 15 s. Lifecycle events (user.*, session.*) retry up to 5 times with exponential backoff (30 s, 2 min, 10 min, 1 h, 6 h) until you respond 2xx. Use X-OTL-Event-Id as your idempotency key — the same id will be sent on every retry.
Events emitted
| Event | Fires when |
|---|---|
| user.created | A new user registers anywhere in the network. |
| user.updated | Profile fields change on any partner site. |
| user.account.deleted | User invokes account deletion (CCPA / GDPR erasure). Implemented. |
| session.created | A user logs in to one of your sites. |
| session.revoked | A session is logged out or admin-revoked. |
| back_channel_logout | Single Logout (SLO) broadcast to every partner site holding the session. |
| role.assigned | A user is granted a role on your site. |
| payment.completed | A paid plan order is fulfilled. |
Payload schema
All events share the same envelope. data varies by event type.
{
"event_type": "user.account.deleted",
"timestamp": "2026-04-25T18:30:00Z",
"data": {
"otl_subject_id": "01HKZ...",
"reason_code": "user_initiated_erasure",
"compliance_flags": {
"ccpa_drop_request": false,
"gdpr_erasure_request": true
}
},
"metadata": {
"partner_client_id": "<your_site_domain>",
"action_required": "Sever SSO link and execute local data retention/deletion policies."
}
}
Signature verification (Python example)
import hmac, hashlib
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
# signature_header looks like "sha256=<hex>"
if not signature_header.startswith("sha256="):
return False
received = signature_header.split("=", 1)[1]
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(received, expected)
Endpoints
/api/v1/webhooksRegister an endpoint/api/v1/webhooks/eventsList recent delivery attempts/api/v1/webhooks/events/{event_id}/replayReplay a single eventReview delivery semantics in the webhooks reference before pointing at your prod endpoint.
Sandbox keys
Sandbox is a separate site you create alongside your prod site — same dashboard, distinct site_key and webhook_secret. Use it for local dev, CI, and load tests.
How to get sandbox credentials
- Sign in to your dashboard at
/owner/dashboard. - Open Settings → Sites and click Add a sandbox site. Use a name like
acme-sandboxand a callback URL onlocalhostor a staging host. - Copy the
site_key(prefixotr_) into your dev environment's secret manager. The sandboxwebhook_secretis shown once on the same screen. - Sandbox sites are free of charge — no per-registration fee — and live alongside your prod site under the same account.
Sandbox vs production
| Capability | Sandbox | Production |
|---|---|---|
| All REST endpoints | Yes | Yes |
| Webhook deliveries | Yes | Yes |
| Localhost / HTTP callback URLs | Yes | No (HTTPS only) |
| Per-registration billing | No (free) | $0.05 / new reg, capped at $499/mo |
| Real email / SMS delivery | No (logged only) | Yes |
| Rate limit | 60 req / min / site_key | 60 req / min / site_key |
| Data retention | 30 days, may be wiped between releases | Per your retention policy |
Sandbox keys never authenticate against the production database, and production keys never authenticate against sandbox — the boundary is enforced by site_key.
Integration checklist
Step-by-step pre-go-live tasks. Work through these in order against your sandbox site.
- Register your prod site and a sandbox site — receive both
site_keys. - Store keys securely — AWS Secrets Manager, Vault, or your equivalent. Never commit to git.
- Implement
/auth/login— call withX-OTL-Site-Key, parse the access + refresh token response. - Implement refresh-token rotation — on
401, call/auth/refreshand replace the stored refresh token with the new one. - Configure your callback URL — the URL OneTimeLogin redirects to after login. HTTPS in prod;
localhostallowed in sandbox. - Stand up a webhook receiver — verify
X-OTL-Signaturewith constant-time HMAC compare, and respond2xxin under 5 seconds. - Handle
user.account.deleted— sever the SSO link and run your local retention / deletion policies. Required for CCPA / GDPR. - Wire up logout — call
/auth/logouton user action. Optionally subscribe toback_channel_logoutfor cross-site SLO. - Add error logging — record the
request_idon every non-2xx response. We use it to look up the matching server log. - Bulk-import existing users (optional) —
POST /api/v1/bulk/registerwith a CSV. Imported users do not count toward billing. - Run end-to-end tests — happy path, expired token, revoked session, webhook signature mismatch, rate-limit (60 / min / site_key).
Go-live checklist
Final checks before flipping the production site_key on. Run all of these against your prod site, not sandbox.
- Callback URL is HTTPS — cert valid, no mixed content.
- Production
site_keyis in your secret manager — never in app code, never in env files committed to git. - Webhook signature verification is on — tests pass with the correct secret and fail with a tampered one.
- Idempotency on webhooks — replays of the same
X-OTL-Event-Idare no-ops. - Refresh-token rotation is wired — old refresh tokens are discarded after a successful refresh.
- Rate-limit handling — on
429, you honorRetry-Afterrather than retrying immediately. - Account-deletion handler is live —
user.account.deletedtriggers your local user-record purge. - Existing users imported (if migrating) — via
/bulk/registerso re-logins don't count as new registrations. - Monitoring is on — alert on 5xx rates from OneTimeLogin and on signature-verification failures from your webhook receiver.
- Privacy policy + DPA reflect the new processor — if applicable to your jurisdiction. Email legal@onetimelogin.com for our DPA.
- Rollback plan documented — how you revert to the legacy login flow if something regresses in the first hour.
Want a 20-minute go-live review with an engineer? Email hello@onetimelogin.com.
Bulk Registration
Migrate users from your existing system. Upload a CSV; we return a job_id you can poll.
/api/v1/bulk/registerUpload CSV (multipart/form-data)/api/v1/bulk/jobs/{job_id}Status, success count, error rowsPayments
Read-only access to plan orders for the authenticated user.
/api/v1/payments/ordersList orders/api/v1/payments/orders/{order_id}Retrieve one orderRate limits
- Default: 60 requests / minute /
site_key. - Auth endpoints: 10 requests / minute / IP.
- Burst: 2x for 5 seconds.
- When exceeded: we return
429 Too Many Requests. Honor theRetry-Afterheader.
Need higher limits? Contact hello@onetimelogin.com.
Errors
Every error response is JSON in this shape:
{
"detail": "<human-readable message>",
"request_id": "<uuid>"
}
Always log request_id — we use it to look up the matching server log when you write to support.
| Code | Meaning |
|---|---|
| 400 | Bad Request — malformed JSON or missing field |
| 401 | Unauthorized — token missing, expired, or revoked |
| 403 | Forbidden — token valid but insufficient role |
| 404 | Not Found |
| 409 | Conflict — duplicate key or stale version |
| 422 | Validation Error — field-level failure |
| 429 | Too Many Requests — honor Retry-After |
| 500 | Internal Server Error |
| 502 | Bad Gateway — upstream IdP unreachable |
| 503 | Service Unavailable — check /status |
Versioning
/api/v1/is the current stable version./v1/is a permanent alias of/api/v1/.- Breaking changes ship as
/v2/; non-breaking additions land in/v1/. - Deprecation policy: 6 months minimum notice before any version is removed, communicated via email and the
Deprecationresponse header.
OpenAPI spec
The machine-readable OpenAPI 3.1 spec is available to authenticated customers on request. We don't expose it on a public URL because the full route surface includes admin / internal endpoints we audit separately.
Email support@onetimelogin.com with your account ID for the customer-scoped spec, or open a ticket from /support.
Have a question?
Email developers@onetimelogin.com — real engineers, no ticket triage.