Skip to main content

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 generic orbit.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):
2. Render the secret — show the 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):
4. Recovery codes — POST /api/v1/verify/factors/totp/{factor_id}/recovery-codes/regenerate invalidates the prior set and re-mints 10 codes (surfaced once):
200
5. Revoke — DELETE /api/v1/verify/factors/totp/{factor_id} returns 204 No Content (idempotent — deleting a missing factor is safe):
TOTP writes (create, verify, delete, recovery-code regeneration) require the 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’s factor.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:
2. Issue a challenge — 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:
3. Verify the signed nonce — the device signs the challenge; submit the signature with POST /api/v1/verify/push/challenges/{challengeId}/verify:
4. Revoke the device — POST /api/v1/verify/push/factors/{factorId}/revoke un-pairs the device (after revoke, new challenges are rejected with 409 FACTOR_REVOKED; irreversible):
Every push endpoint is gated by authentication and per-tenant rate limiting only — no 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:
2. Complete the enrollment — run the browser’s navigator.credentials.create(), then POST /api/v1/verify/passkey/registration/verify with the attestation response. A verified attestation creates the factor:
3. Start authentication — POST /api/v1/verify/passkey/authentication/options returns the PublicKeyCredentialRequestOptionsJSON for navigator.credentials.get(), with the identity’s known credentials hinted in allowCredentials:
4. Verify the assertion — 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:
5. Revoke — POST /api/v1/verify/passkey/factors/{factorId}/revoke removes the passkey:
Like push, every passkey endpoint is gated by authentication and per-tenant rate limiting only — no extra scope.

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:
2. Verify — 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:
3. Delete — DELETE /api/v1/verify/factors/backup-codes/{factor_id} removes the factor (204 No Content, idempotent):
Backup-code writes require the verify:write scope (same posture as TOTP); reads are authentication-gated only. 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:
2. Consume — the user’s click hits the public, unauthenticated endpoint GET /public/verify/magic-link/consume?token={token}, which approves the underlying verification on success and rejects tampered, expired, or already-used tokens:
200
The token is a constant-time HMAC-signed payload. There is nothing to enroll or revoke — the link is single-use per verification, so treat it as a send/check variant rather than a persistent factor.

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:write scope. 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:write or verify:read scope, so any authenticated key scoped to the tenant can call them.
Issue dedicated keys for factor flows and restrict them to the callers you actually want enrolling and revoking factors. Tenant isolation applies throughout — a factor created by one tenant is never visible to another.

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_count on 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 sna check 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.
Pick the factor where a device or passkey exists; fall back to OTP delivery where it does not.

See also