The Verify factor suite end to end
Verify ships possession-of-secret MFA factors alongside OTP delivery: TOTP (authenticator apps), push (device public key + signed challenge), passkeys (WebAuthn / FIDO2), magic links, and backup codes. This guide walks the enroll → verify → revoke lifecycle for each one, so you can wire factor flows into your own application instead of assembling the endpoint tables yourself. The factor suite is API-only by design — it has no dashboard screen. Factors are enrolled, verified, and revoked from inside your application, where the end user is present to scan a QR code, run a WebAuthn ceremony, or approve a push prompt on their device. Drive the endpoints directly or through the SDK. (The Verify dashboard covers OTP send/check, profiles, and analytics; the 2FA control under Settings → Security protects your Orbit login and is separate from these end-user factors.) Factor endpoints carry no carrier delivery and no wallet deduct — they validate proof-of-possession against a server-side secret or device key pair. Every factor endpoint requires an authenticated request and is isolated to your organization: a factor created by one tenant is never visible to another.1. Delivery channels vs factor channels
POST /verify/send accepts both a delivery channel and a factor channel value, but they mean different things. The first group delivers an OTP over a carrier; the second group proves possession without carrying an OTP.
Delivery channels — sms, whatsapp, email, voice, viber, telegram, rcs, flashcall:
The send generates a code and hands it to the channel provider. Phone-shaped recipients for phone channels, email-shaped recipients for email. These are also the only channels that make sense in a fallback chain.
Factor channels — totp, push, passkey ceremonies, backup_code, plus the device-possession checks sna and silent, and magic_link:
There is no OTP in transit and nothing to be delivered along a fallback chain. sna (Silent Network Authentication) and silent are network-level possession lookups rather than enrollable factors; totp, push, and backup_code are enrollable factors with their own lifecycle endpoints, covered below. magic_link replaces the typed code with a single-use email link (see §3.5).
2. Why /verify/send rejects a factor with 422
Sending channel: "totp", "push", or "backup_code" to POST /verify/send does not issue an OTP. The send endpoint short-circuits a factor-channel request with a typed 422 / 400 response that points you at the matching factor endpoint, because those channels are possession-of-secret factors with their own lifecycle — there is no carrier leg to send on, and no code to check with /verify/check afterward.
To enroll or exercise a factor, call its dedicated endpoint directly (§3). To use a network-level possession check (sna, silent) or a magic link, those remain valid /verify/send channels — only the enrollable factors are rejected.
3. Enroll, verify, and revoke — per factor
Each subsection is the full lifecycle for one factor: enroll/create, exercise, revoke. The raw endpoint tables live in the Verify overview; the samples below put the calls in operator order with runnable curl plus the Node SDK genericorbit.request fallback (the SDK escape hatch, since these flows have no typed SDK helpers). All examples authenticate with an API key; http://localhost:4010 is only for local runs.
3.1 TOTP (authenticator app)
RFC 6238 factors compatible with Google Authenticator, Authy, 1Password, and similar apps. Codes are 6 digits on a 30-second step with ±1 step of clock-skew tolerance. 1. Enroll —POST /api/v1/verify/factors/totp. The response returns the otpauth:// URI for QR rendering, the base32 secret for manual entry, and 10 single-use recovery codes — all surfaced once (subsequent reads return metadata only):
- cURL
- Node.js SDK
otpauth_uri as a QR code (or the secret for manual entry) while the user is present.
3. Verify — POST /api/v1/verify/factors/totp/{factor_id}/verify with the current 6-digit code. The same endpoint also accepts a recovery code from the minted set; the response’s method tells you which path matched (totp or recovery_code):
- cURL
- Node.js SDK
POST /api/v1/verify/factors/totp/{factor_id}/recovery-codes/regenerate invalidates the prior set and re-mints 10 codes (surfaced once):
200
DELETE /api/v1/verify/factors/totp/{factor_id} returns 204 No Content (idempotent — deleting a missing factor is safe):
verify:write scope; dashboard session callers also need an owner, admin, or developer role. Reads (list) are gated by authentication and per-tenant rate limiting only.
3.2 Push (device public key)
Phishing-resistant push MFA via a device public key and a signed-challenge flow (mirroring Twilio Verify’sfactor.type = "push"). The device holds the private key; the server keeps the public key and verifies signatures.
1. Create the factor — POST /api/v1/verify/push/factors registers the device’s public key and returns a binding challenge the device must sign to move the factor from unverified to verified:
- cURL
- Node.js SDK
POST /api/v1/verify/push/factors/{factorId}/challenges mints the nonce the device must sign. The signing string is <factor_id>.<challenge_id>.<nonce> — sign the raw bytes, never a JSON envelope:
- cURL
- Node.js SDK
POST /api/v1/verify/push/challenges/{challengeId}/verify:
- cURL
- Node.js SDK
POST /api/v1/verify/push/factors/{factorId}/revoke un-pairs the device (after revoke, new challenges are rejected with 409 FACTOR_REVOKED; irreversible):
- cURL
- Node.js SDK
verify:write scope is required, so restrict which API keys reach these flows accordingly.
3.3 Passkeys (WebAuthn / FIDO2)
Phishing-resistant passkey factors. The browser or OS handles the ceremony; the server only issues the options payload, verifies the attestation/assertion response, and keeps the credential state. 1. Start registration —POST /api/v1/verify/passkey/registration/options returns the PublicKeyCredentialCreationOptionsJSON the FE hands to navigator.credentials.create(). Hand the full options object straight through — no field mutation:
- cURL
- Node.js SDK
navigator.credentials.create(), then POST /api/v1/verify/passkey/registration/verify with the attestation response. A verified attestation creates the factor:
- cURL
- Node.js SDK
POST /api/v1/verify/passkey/authentication/options returns the PublicKeyCredentialRequestOptionsJSON for navigator.credentials.get(), with the identity’s known credentials hinted in allowCredentials:
- cURL
- Node.js SDK
POST /api/v1/verify/passkey/authentication/verify validates the assertion and bumps the counter. On a counter regression (cloning signal) the factor is auto-revoked:
- cURL
- Node.js SDK
POST /api/v1/verify/passkey/factors/{factorId}/revoke removes the passkey:
- cURL
- Node.js SDK
3.4 Backup codes
Single-use recovery codes for users who lose their device and SMS access. These are the natural recovery leg for any of the factors above. 1. Mint —POST /api/v1/verify/factors/backup-codes returns 10 plaintext codes once; the server stores SHA-256 hashes only:
- cURL
- Node.js SDK
POST /api/v1/verify/factors/backup-codes/{factor_id}/verify consumes one code. Each code can succeed at most once, and the response returns remaining_count so your UI can prompt regeneration before the user runs out:
- cURL
- Node.js SDK
DELETE /api/v1/verify/factors/backup-codes/{factor_id} removes the factor (204 No Content, idempotent):
verify:write scope (same posture as TOTP); reads are authentication-gated only.
3.5 Magic link
A magic link replaces the typed code with an email-delivered, single-use HTTPS link (Stytch / WorkOS / Auth0 parity). 1. Send —POST /api/v1/verify/send with channel: "magic_link" and an email to. Orbit emails a signed link; the <Note> in the Verify overview covers the same endpoint lookup pattern the /check resolver uses:
- cURL
- Node.js SDK
GET /public/verify/magic-link/consume?token={token}, which approves the underlying verification on success and rejects tampered, expired, or already-used tokens:
200
4. Securing factor endpoints
The scope split matters for how you issue API keys:- TOTP and backup-code writes (create, verify, delete, recovery-code regeneration) require the
verify:writescope. Dashboard session callers must also hold an owner, admin, or developer role. - TOTP and backup-code reads, and every push and passkey endpoint, are gated by authentication and per-tenant rate limiting only. They do not require an additional
verify:writeorverify:readscope, so any authenticated key scoped to the tenant can call them.
5. Data model and recovery
- Secrets surface once. TOTP returns the base32 secret and
otpauth://URI on create; backup codes return the plaintext set on create. Persist them to the enrolling user’s device or your recovery flow at that moment — later reads return metadata only. - Hashes at rest. Backup codes (and TOTP recovery codes) are stored as SHA-256 hashes; verify compares hashes, never plaintext.
- Recovery codes as the fallback leg. Pair any primary factor (TOTP, push, passkey) with a backup-code factor so a user who loses a device can still sign in. Monitor
remaining_counton backup-code verify responses and prompt regeneration before exhaustion. - Revoke-a-device runbook. To cut off a lost or compromised device: revoke the push factor (
/verify/push/factors/{id}/revoke) or passkey (/verify/passkey/factors/{id}/revoke), delete the TOTP factor, and — if the enrollment used backup codes — delete that factor or regenerate the TOTP recovery codes. Revocation endpoints are idempotent-safe to retry where noted (backup-code delete).
6. When to pick a factor over an OTP channel
OTP delivery channels and factors answer different threats:- Phishing-resistant possession — passkeys and the network-level
snacheck are not relayable through a phishing proxy in the way a typed OTP is. Prefer them for account-recovery and step-up flows where an attacker who can relay an OTP would otherwise win. - Device-bound approval — push proves a specific paired device answered, not just that someone read a code.
- Nothing to relay — TOTP and backup codes keep the secret on both ends with no carrier leg, so no delivery provider, no SIM-swap exposure in transit, and no wallet deduct.
- OTP still fits when the recipient has no enrolled device and you need to reach them over SMS, WhatsApp, email, or voice — the delivery channels and fallback chains cover that reach.
See also
- Verify overview — channel catalog, send/check reference, and the full MFA factor endpoint tables.
- Verify profiles: fallback chains and advanced factors — pair factors with OTP profiles and ordered fallback chains.
- Verify integration without our SDK — plain-HTTP send/check with the full error matrix.
- Verify API reference — every endpoint, including profile CRUD.
- Push factor API reference and Passkey API reference — full request/response shapes for those suites.