Factor lifecycle
Verify ships four possession-of-secret MFA factors alongside the carrier-delivered OTP: TOTP (authenticator app), passkey (WebAuthn / FIDO2), push (device public-key + signed challenge), and backup codes (single-use recovery). None of the four carry a carrier delivery or a wallet deduct — each validates proof-of-possession against a server-side secret or device key pair. This page is the reference for how a factor moves from enrollment through verification to retirement, and how the four compose into one factor chain. For the onboarding walkthrough, see Verify profiles: fallback chains and advanced factors; for the endpoint list, see the Verify overview.The factor suite is API-only — there is no dashboard screen. TOTP, backup codes, passkeys, and push are enrolled, verified, and revoked from inside your own application, where the end user is present to scan a QR code, run a WebAuthn ceremony, or approve a push prompt. Drive them straight from the endpoints below or the SDK.
What a factor is
A factor is a server-side record that proves an end user possesses a secret only they should hold. Every factor belongs to one tenant and one user; a factor created by one tenant is never visible to another. The four factors split by where the secret lives:
Pick passkey or push for phishing-resistant step-up; pick TOTP for the broadest device compatibility (any RFC 6238 authenticator); pick backup codes as the recovery lane for users who have lost their device and their carrier access.
Factors vs the carrier delivery chain
The four factors are independent sibling endpoints — each has its own enrollment, verification, and revocation path under/verify/factors/*, /verify/push/*, and /verify/passkey/*. There is no platform-side chain that falls back across these factors on a declined verification. The only fallback chain Verify ships is the carrier delivery chain (channels[] on a profile), which falls back across delivery channels (SMS → voice → email) when a send times out — it does not fall back across the possession factors, and a factor verification never falls back to a carrier OTP.
What /verify/send does with a factor channel: sending channel: "totp", "push", or "backup_code" to POST /verify/send does not issue an OTP or enter the delivery chain. The send endpoint short-circuits with a typed 422 (for totp and backup_code) or 400 (for push) that points your integration at the matching factor endpoint — those channels are possession-of-secret factors with their own lifecycle, not delivery channels. Composing a step-up flow across factors is your integration’s design: your app decides which factor to try, calls its endpoint, and falls back to a different factor (or to backup codes for recovery) in your own code. The platform gives you the four endpoints; it does not chain them for you.
TOTP — authenticator app
RFC 6238 time-based one-time password factors (Google Authenticator, Authy, 1Password, etc.). The factor is a shared symmetric seed; the server and the device each derive the same 6-digit code from the current 30-second step.Enroll
POST /verify/factors/totp creates the factor and returns the otpauth:// provisioning URI for QR rendering plus the base32 secret once. Subsequent reads return metadata only — the plaintext secret is never re-exposed after creation. Ten single-use recovery codes are minted on the same create call and surfaced once; only their SHA-256 hashes are persisted.
Verify
POST /verify/factors/totp/{id}/verify validates a 6-digit code against the current step with a ±1 step skew (the prior and next 30-second windows are accepted to absorb clock drift). A match returns the factor’s status; a mismatch does not lock the factor on a single bad code — there is no in-factor attempt counter and no lockout, so a fresh code in the next step window retries cleanly. Tenant-level request rate limiting (shared with the other Verify write endpoints) still throttles a client that hammers verify, but the TOTP factor itself never goes “locked.”
Retire
DELETE /verify/factors/totp/{id} removes the factor. POST /verify/factors/totp/{id}/recovery-codes/regenerate invalidates every prior recovery code and mints a fresh set of 10 — call it when a recovery code set may have been exposed, not on a schedule.
Backup codes
Single-use, hashed-at-rest recovery codes for users who have lost their device and carrier access. The plaintext exists exactly long enough to show it once; the server stores SHA-256 hashes only.Enroll
POST /verify/factors/backup-codes mints 10 plaintext codes returned once. Each code can succeed at most once, enforced by an UPDATE ... WHERE consumed_at IS NULL gate — the verify call atomically marks the code consumed and rejects a second use of the same code.
Verify
POST /verify/factors/backup-codes/{id}/verify consumes one code and returns remaining_count, so your UI can prompt regeneration before the set runs out. A verify against an exhausted or already-consumed code is rejected — there is no retry window on a backup code, because each code is single-use by construction.
Retire
DELETE /verify/factors/backup-codes/{id} is idempotent. Regenerate by creating a new factor; a backup-code factor is the recovery lane, not a primary factor — re-mint when the remaining_count is low.
Push — device public key
Phishing-resistant MFA via a device public key and a signed challenge (mirrors Twilio Verifyfactor.type="push"). The private key never leaves the device; the server verifies a signature over a server-issued nonce.
Enroll
POST /verify/push/factors registers a device public key and returns the factor id. The factor is inert until a challenge is issued and verified.
Verify
The two-step flow is issue (POST /verify/push/factors/{factorId}/challenges) then submit (POST /verify/push/challenges/{challengeId}/verify). The device signs a server-issued nonce with its private key; the server verifies the signature against the registered public key. Each challenge has max_attempts = 3 and a default TTL of 120s (capped at 600s) — the attempts counter increments on every verify call before the signature is checked, and a third failed attempt moves the challenge to denied. An expired challenge moves to expired; a stale, already-decided, or revoked factor is rejected. Push dispatch to the device runs through the push dispatcher (FCM / APNs / Web Push) with a polling fallback; a challenge the user never answers expires and a fresh challenge must be issued.
Retire
POST /verify/push/factors/{factorId}/revoke revokes the paired device and invalidates its outstanding challenges.
Passkey — WebAuthn / FIDO2
Phishing-resistant passkey factors. The registration and authentication ceremonies follow the WebAuthn spec; attestation and assertion verification (CBOR / COSE / signature) runs server-side.Enroll
POST /verify/passkey/registration/options issues the WebAuthn registration ceremony options; POST /verify/passkey/registration/verify verifies the attestation and creates the factor. The credential’s public key is stored server-side; the private key stays on the device (or hardware security module).
Verify
The two-step flow is options (POST /verify/passkey/authentication/options) then verify (POST /verify/passkey/authentication/verify). Each ceremony has max_attempts = 3 and a default challenge TTL of 300s (capped at 600s); a denied assertion is terminal and a third failed attempt ends the ceremony. A successful assertion bumps the per-credential counter — a counter that does not advance (a replay) auto-revokes the factor, because a credential’s counter must strictly increase across authentications. The origin binding is verified on every assertion, so a passkey registered for auth.example.com cannot be used from a different origin.
Retire
POST /verify/passkey/factors/{factorId}/revoke revokes the paired passkey. A revoked passkey’s assertions are rejected at the options stage.
When a given factor is the right choice
Backup codes are the recovery lane for every other factor — mint them when the user enrolls a TOTP, passkey, or push factor, and surface
remaining_count so the user regenerates before the set runs out.
Retry and lockout per factor
No factor has a global “locked” flag that disables the whole factor type — the lockout is per-code, per-challenge, or per-assertion, and a fresh issuance clears it. The TOTP factor has no per-factor lockout at all: a mismatched code costs nothing but a wait for the next step. Tenant-level request rate limiting applies on top of all four, so a client that hammers any
verify endpoint is throttled regardless of the per-factor semantics.
Scope and isolation
Write operations for TOTP and backup-code (create, verify, delete, and recovery-code regeneration) require theverify:write scope; dashboard session callers must also hold an owner, admin, or developer role. TOTP and backup-code read endpoints, plus every push and passkey endpoint, are gated by authentication and per-tenant rate limiting only — any key scoped to the tenant can call them, so restrict which keys reach these flows. A factor created by one tenant is never visible to another; the factor record is tenant-isolated at the data layer.
Related
- Verify overview — channel capabilities, send/check, and the factor endpoint table
- Verification configuration — profiles, templates, expiry, attempts, and the carrier fallback chain
- OTP approvals — the two-officer gate for template edits and resend bursts
- Verify profiles: fallback chains and advanced factors — the onboarding walkthrough that pairs the carrier chain with the factor chain
- Verify API reference — every factor endpoint