Two-step Verification

Manage two-step verification for a signed-in account: turn the switch on or off, enroll authenticator apps and passkeys, issue recovery codes, and rename or remove methods. Finish a pending sign-in or re-authenticate a browser session through the shared verify endpoints (documented under Login second factor and Magic link sign-in).

The workspace app exposes this under Settings → Security. Browser sign-ins that need a second factor redirect to /auth/verify with a challenge_token query parameter.

→ Authentication · Magic link sign-in · Device authorization


When a second factor is required

AccountAt sign-inSensitive browser actions
Platform staff (admin role)Always. Until an authenticator app or passkey is enrolled, only an emailed code is offered (no platform admin powers until an authenticator is proved).Re-auth with an enrolled authenticator when a route requires it (403 otp_required or 403 admin_authenticator_required).
MembersOnly when two-step verification is turned on in settings.Same re-auth rules when two-step verification is on.

Two-step verification cannot be turned off for platform staff. Members need at least one enrolled authenticator app or passkey before they can turn the switch on.


Authentication

Endpoint groupCredential
GET /auth/second-factorSession cookie (read-only overview).
POST /auth/second-factor/*, TOTP enroll/confirm, passkey registerSession cookie and a browser session that has been re-authenticated within the last 4 hours when the account requires a second factor. Changing methods without a fresh proof returns 403 otp_required.
GET /auth/otp/status, POST /auth/otp/verify, POST /auth/otp/resend, POST /auth/passkey/assert/beginPending sign-in: challenge_token. Re-auth: session cookie. See Login second factor.

All settings routes require a normal signed-in session; API keys and CLI bearer tokens cannot manage two-step verification.


Overview

GET /auth/second-factor

Returns the account's two-step verification state.

json
1{2  "enabled": true,3  "methods": [4    {5      "id": "sfm_abc",6      "kind": "passkey",7      "name": "MacBook Touch ID",8      "created_at": "2026-10-21T10:00:00Z",9      "last_used_at": "2026-10-21T12:00:00Z",10      "backup_state": true11    }12  ],13  "recovery_codes": {14    "generated_at": "2026-10-21T10:05:00Z",15    "remaining": 1016  },17  "passkeys_available": true18}
FieldDescription
enabledWhether two-step verification is on (User.two_factor_enabled).
methodsEnrolled authenticator apps (kind: "totp") and passkeys (kind: "passkey").
recovery_codesOmitted until a set exists; remaining counts unused codes.
passkeys_availableWhether the server is configured as a WebAuthn relying party.

UserDTO fields

User objects in API responses (for example user on GET /me) expose the same switch:

FieldDescription
two_factor_enabledWhether two-step verification is on. Use this field in new integrations.
totp_enabledDeprecated. Always equal to two_factor_enabled; kept for older clients.

Method kind values used in API responses: totp, passkey. At verify time the API also accepts email (emailed code) and recovery (single-use recovery code).


Turn two-step verification on or off

Enable

POST /auth/second-factor/enable

Turns the switch on. Requires at least one enrolled authenticator app or passkey. When the account has no recovery codes yet, a new set of 10 codes is generated and returned once:

json
1{2  "enabled": true,3  "recovery_codes": ["abcd-efgh-ijkl-mnop", "..."]4}

Subsequent enable calls return "recovery_codes": [] if codes already exist.

Disable

POST /auth/second-factor/disable

Turns the switch off and voids recovery codes. Enrolled methods stay. Platform staff receive 403 admin_authenticator_required.

Response: { "enabled": false }.


Recovery codes

POST /auth/second-factor/recovery-codes

Replaces the current set with 10 new single-use codes. The previous set stops working immediately. Requires re-authenticated browser session.

json
1{2  "codes": ["xxxx-yyyy", "..."],3  "generated_at": "2026-10-21T10:00:00Z"4}

Store codes securely. Each successful POST /auth/otp/verify with "method": "recovery" consumes one code.


Manage methods

EndpointMethodDescription
/auth/second-factor/methods/{id}PATCHRename. Body: { "name": "Work laptop" }.
/auth/second-factor/methods/{id}DELETERemove the method.

Rules enforced by the API:

  • At most 10 methods per account (method_limit, 409).
  • While two-step verification is on, at least one authenticator app or passkey must remain (last_authenticator, 409).
  • Platform staff must always keep at least one authenticator app or passkey (admin_authenticator_required, 403).

Enroll an authenticator app (TOTP)

Two-step enrollment uses a sealed enrollment token between enroll and confirm (expires in 10 minutes). TOTP is not stored until confirm succeeds.

Start enrollment

POST /auth/totp/enroll

json
1{2  "secret": "BASE32SECRET",3  "url": "otpauth://totp/...",4  "enrollment": "opaque-enrollment-token"5}

Scan url in an authenticator app (Google Authenticator, 1Password, and similar) or enter secret manually.

Confirm

POST /auth/totp/confirm

json
1{2  "enrollment": "opaque-enrollment-token",3  "code": "123456",4  "name": "Authenticator app"5}

Returns a SecondFactorMethodDTO for the new method. Optional name defaults on the server when omitted.


Enroll a passkey

Requires passkeys_available: true on the overview. Ceremonies expire in 5 minutes.

Begin registration

POST /auth/passkey/register/begin

json
1{2  "ceremony_id": "cer_abc",3  "options": { /* PublicKeyCredentialCreationOptions JSON */ }4}

Pass options to navigator.credentials.create() in the browser.

Finish registration

POST /auth/passkey/register/finish

json
1{2  "ceremony_id": "cer_abc",3  "name": "MacBook Touch ID",4  "credential": { /* PublicKeyCredential.toJSON() */ }5}

Returns the enrolled method. When passkeys are not available (passkeys_available: false), begin and finish return 503 not_configured.


Login second factor

Used after passwordless sign-in (magic link, login code, OAuth, SAML) and for re-auth on an existing browser session. The same routes handle both cases.

ModeCredential
Pre-session (login)challenge_token from AuthResponse or the /auth/verify URL
Re-authSession cookie (omit challenge_token)
EndpointMethodDescription
/auth/otp/statusGETrequired, verified, methods (default first), optional reauth_until on re-auth.
/auth/otp/verifyPOSTProve the second factor. See Verify body.
/auth/otp/resendPOSTSend a new emailed code when email is among methods.
/auth/passkey/assert/beginPOSTStart passkey assertion for login or re-auth. Body: { "challenge_token": "…" } or {} with session cookie.

Login challenges expire after 10 minutes. After a successful verify, the session is marked re-authenticated for 4 hours (reauth_until).

Wrong codes share one per-account lockout: 10 failures within 15 minutes returns 429 account_locked (second_factor_unavailable when the lockout store is unavailable).

Verify body

POST /auth/otp/verify

json
1{2  "challenge_token": "CHALLENGE_TOKEN",3  "method": "totp",4  "code": "123456"5}
FieldDescription
challenge_tokenRequired for pre-session login; omit for re-auth.
methodOne of email, totp, passkey, recovery. Defaults to the first entry in GET /auth/otp/status → methods.
codeRequired for email, totp, and recovery.
assertionRequired for passkey: { "ceremony_id": "…", "credential": { … } } from assert/begin + navigator.credentials.get().

Pre-session success returns AuthResponse with session_id and sets the session cookie. Re-auth success returns { "verified": true, "reauth_until": "…" }.

Example: emailed code at login:

bash
1curl "https://api.inference.sh/auth/otp/status?challenge_token=CHALLENGE_TOKEN"23curl -X POST https://api.inference.sh/auth/otp/verify \4  -H "Content-Type: application/json" \5  -c cookies.txt \6  -d '{"challenge_token": "CHALLENGE_TOKEN", "method": "email", "code": "123456"}'

Example: passkey at login:

bash
1# 1. Begin assertion2curl -X POST https://api.inference.sh/auth/passkey/assert/begin \3  -H "Content-Type: application/json" \4  -d '{"challenge_token": "CHALLENGE_TOKEN"}'56# 2. Run WebAuthn in the browser, then finish:7curl -X POST https://api.inference.sh/auth/otp/verify \8  -H "Content-Type: application/json" \9  -c cookies.txt \10  -d '{"challenge_token": "CHALLENGE_TOKEN", "method": "passkey", "assertion": {"ceremony_id": "cer_abc", "credential": {}}}'

Errors

Settings and enrollment

CodeHTTPWhen
last_authenticator409Enable or remove would leave two-step on with no authenticator app or passkey
admin_authenticator_required403Platform staff cannot disable two-step or remove their last authenticator
method_limit409Ten methods already enrolled
not_found404Unknown method id
expired400Passkey ceremony or TOTP enrollment expired
not_configured503Passkeys not available on this server
otp_required403Settings change needs a fresh second-factor proof on the session

Verify step

CodeHTTPWhen
invalid_code400Wrong or consumed code
account_locked429Too many failed attempts within 15 minutes
method_not_offered400method not in methods, or resend when email is not offered
missing_code / missing_assertion400Required field missing for the chosen method
invalid_token400Expired or unknown challenge_token
second_factor_unavailable503Lockout store unavailable (fail closed)

we use cookies

we use cookies to ensure you get the best experience on our website. for more information on how we use cookies, please see our cookie policy.

by clicking "accept", you agree to our use of cookies.
learn more.