> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# MFA factor suite: TOTP, push, passkeys, magic links, and backup codes

> Enroll, verify, and revoke possession-of-secret MFA factors end to end — TOTP authenticator apps, push device keys, WebAuthn passkeys, email magic links, and single-use backup codes.

# 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](/guides/verify-fallback-chains).

**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. The raw endpoint tables for every factor live in the [Verify overview](/verify/overview#mfa-factors); the steps below put them in operator order.

### 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 plus the base32 secret **once** — subsequent reads return metadata only. Ten single-use recovery codes are minted on creation and surfaced once; only their SHA-256 hashes are persisted.
2. **Render the secret** — show the OTPauth URI as a QR code (or the base32 secret for manual entry) while the user is present.
3. **Verify** — `POST /api/v1/verify/factors/totp/{id}/verify` with the current 6-digit code from the user's app. A match approves the factor challenge.
4. **Recovery codes** — `POST /api/v1/verify/factors/totp/{id}/recovery-codes/regenerate` invalidates the previous set and re-mints 10 codes.
5. **Revoke** — `DELETE /api/v1/verify/factors/totp/{id}` removes the factor.

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"`).

1. **Register the device** — `POST /api/v1/verify/push/factors` with the device's public key.
2. **Issue a challenge** — `POST /api/v1/verify/push/factors/{factorId}/challenges` creates a new challenge for the paired device.
3. **Verify the signed nonce** — the device signs the challenge nonce; submit it 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.

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 registration and authentication ceremonies follow the WebAuthn spec; attestation and assertion verification (CBOR / COSE / signature) runs server-side.

1. **Start registration** — `POST /api/v1/verify/passkey/registration/options` issues a WebAuthn registration ceremony (challenge, relying-party parameters).
2. **Complete enrollment** — run the browser's `navigator.credentials.create()` ceremony, 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` issues an authentication ceremony for an enrolled passkey.
4. **Verify the assertion** — `POST /api/v1/verify/passkey/authentication/verify` validates the assertion and bumps the sign counter.
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/{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/{id}` removes the factor (idempotent).

Backup-code writes require the `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](/verify/overview#mfa-factors) 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.

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](/verify/overview#channel-capability-at-a-glance) and [fallback chains](/guides/verify-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

* [Verify overview](/verify/overview) — channel catalog, send/check reference, and the full MFA factor endpoint tables.
* [Verify profiles: fallback chains and advanced factors](/guides/verify-fallback-chains) — pair factors with OTP profiles and ordered fallback chains.
* [Verify integration without our SDK](/guides/verify-no-sdk) — plain-HTTP send/check with the full error matrix.
* [Verify API reference](/api-reference/endpoints/verify) — every endpoint, including profile CRUD.
* [Push factor API reference](/api-reference/endpoints/verify-push) and [Passkey API reference](/api-reference/endpoints/verify-passkey) — full request/response shapes for those suites.
