Skip to main content

Verify profiles: fallback chains and advanced factors

A verify profile is a reusable, tenant-owned configuration object that controls how OTP verifications behave: which channels the code travels on, how long the code is, how long it stays valid, how many wrong guesses are allowed, and what the message says. You create a profile once, then pass its profile_id on every /verify/send call that should follow those rules. This guide walks through the profile lifecycle end to end — create a profile, attach an ordered fallback chain, customize code length, TTL, and templates, send and check a code against it — and then covers the factor suite (push, TOTP, passkey, voice biometrics) that runs alongside OTP profiles. Everything below is under your control as the tenant. Profiles only apply to API keys and verifications in your own organization; nothing here is mandated by the platform.

Before you start

  • A live API key (dv_live_sk_…). Profiles are write-guarded: the key must carry the verify:write scope (dashboard callers need an owner, admin, or developer role).
  • The channel providers you plan to use must be connected first. A profile that lists whatsapp is rejected with WHATSAPP_NOT_CONNECTED until a WhatsApp Business Account finishes onboarding; voice, telegram, viber, and email each require their own provider configuration. Keep the chain to channels your tenant can actually deliver on — the API rejects a profile that references an unconnected channel with a 422 VALIDATION_ERROR naming the channel.

1. Create a verify profile

POST /api/v1/verify/profiles creates the profile. Minimum viable body:
Response — 201
Store the returned id (format vprof_<hex>) — you pass it as profile_id on /verify/send.

Profile fields

Update any subset of these later with PATCH /api/v1/verify/profiles/{id}; delete with DELETE. The /configs paths are an alias of /profiles with identical behavior — use whichever your integration already references.
Profile usage is optional everywhere. A /verify/send call with no profile_id falls back to platform defaults (6-digit code, 600s TTL, 3 attempts, channel chosen per send). Profiles exist so you can pin behavior once instead of repeating per-send parameters.

2. Channel fallback chains

The channels array is the fallback chain. Index 0 is the channel the first send attempt uses; the rest are tried in order when the previous channel cannot deliver.
reads as: try SMS first; if SMS rejects or never confirms delivery, move to WhatsApp; if that also fails, place a voice call that reads the code aloud.

How attempts advance

Fallback advances are asynchronous. The initial /verify/send call fires only the first channel. A background engine then advances through the remaining channels when either condition trips:
  • Delivery failure — the provider for the current channel rejected or failed the outbound message.
  • Timeout — no successful delivery confirmation within channel_timeout_seconds (configurable per send via fallback_config, 10–600s, default 60s).
The same OTP is re-delivered on each channel — the platform never re-generates the code on a fallback advance, so a recipient who reads the SMS and the follow-up voice call hears and sees one identical code. Only delivery channels make sense in a chain: sms, whatsapp, email, voice, viber, telegram, rcs, flashcall. Factor-style channels (totp, push, passkey ceremonies, backup_code, sna, magic_link) have their own endpoints (§4) and do not participate in the async chain.

Voice in the chain requires a voice template

If voice appears anywhere in channels, the profile must carry a templates.voice script containing the {{code}} placeholder. The voice OTP is read aloud by a TTS greeting, so the template is the script the recipient hears. Saving a voice-bearing profile without one is rejected:

Watching a chain run

Subscribe to verification.fallback_triggered to observe each advance — one event per channel hop, carrying the channel just entered and its chain index. verification.fallback_exhausted fires when every channel in the chain failed without an approval; treat it as a hard failure and surface a “verify another way” prompt to the user. Both events, plus the rest of the verification.* set, are listed in the webhook events reference.

3. Code length, TTL, and templates

Code length and TTL

codeLength accepts only 4, 6, or 8 digits; expirySeconds is 30–3600 seconds. Short codes with short TTLs fit high-traffic login flows where you want the verification row to expire fast; longer codes with longer TTLs fit email-delivered OTPs and lower-frequency flows where a recipient might take minutes to switch apps. The TTL shows up on the send response as expires_at. After that moment, /verify/check returns EXPIRED_TOKEN (410) and the row can no longer be approved — create a fresh verification rather than re-checking.

Per-channel templates

templates is a map of channel name → message body. Each body may reference these placeholders, resolved at send time: Any other {{…}} token is delivered verbatim (it is not substituted), so a typo in the placeholder name silently reaches the recipient.

4. Push factor, TOTP, and passkeys

Beyond OTP delivery, Verify ships factor endpoints for proof-of-possession MFA. These have no carrier delivery and no wallet charge — they validate a device-held secret instead of a code in transit. All factor endpoints are per-tenant and API-only by design (enroll, verify, and revoke from inside your own application where the end user is present). TOTP and backup-code write operations require the verify:write scope. Backup codes are the natural recovery leg for users enrolled in any other factor who lose their device. The Verify overview lists every endpoint in each factor suite with its method and path.

5. Voice biometrics (optional)

Voice biometrics is an optional, separate surface under /api/v1/verify/voice-biometrics that verifies a caller by their voiceprint rather than by a code. It is independent of OTP profiles — you use it alongside them, not inside a fallback chain. The flow is two calls:
  1. EnrollPOST /api/v1/verify/voice-biometrics/enroll captures a caller’s voice samples and builds a voiceprint (id returned; poll GET /enroll/{id} for status).
  2. Verify — on a later call, POST /api/v1/verify/voice-biometrics/challenge issues a challenge phrase, then POST /api/v1/verify/voice-biometrics/verify scores the fresh sample against the enrolled voiceprint.
Tenant-level behavior (score thresholds, attempt policy) is set on PATCH /api/v1/verify/voice-biometrics/settings; GET /overview summarizes fleet state. The full endpoint shapes are in the voice biometrics API reference.

6. Send and check against the profile

With a profile created, pass its id on send:
The send inherits the profile’s chain, code length, TTL, max attempts, and templates. The channel you pass on the send body is tried first; on a delivery rejection the remaining profile chain entries are tried in profile order. Passing a channel that is not in the profile’s chain tacks it on front of the chain rather than erroring — pass the chain’s first entry to keep behavior aligned with the profile as designed. Check the code exactly as you would without a profile:
status: "approved" means the profile’s rules passed end to end. The full send/check error matrix (attempts_remaining, expiry, resend semantics) is covered in Verify integration without our SDK.

Troubleshooting

See also