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 itsprofile_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 theverify:writescope (dashboard callers need an owner, admin, or developer role). - The channel providers you plan to use must be connected first. A profile that lists
whatsappis rejected withWHATSAPP_NOT_CONNECTEDuntil a WhatsApp Business Account finishes onboarding;voice,telegram,viber, andemaileach 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 a422 VALIDATION_ERRORnaming the channel.
1. Create a verify profile
POST /api/v1/verify/profiles creates the profile. Minimum viable body:
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
Thechannels 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.
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 viafallback_config, 10–600s, default 60s).
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
Ifvoice 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 toverification.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:
- Enroll —
POST /api/v1/verify/voice-biometrics/enrollcaptures a caller’s voice samples and builds a voiceprint (id returned; pollGET /enroll/{id}for status). - Verify — on a later call,
POST /api/v1/verify/voice-biometrics/challengeissues a challenge phrase, thenPOST /api/v1/verify/voice-biometrics/verifyscores the fresh sample against the enrolled voiceprint.
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: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
- Verify overview — channel catalog, send/check reference, MFA factor endpoint list.
- Verify integration without our SDK — the plain-HTTP send/check flow, webhooks, rate limits, and error envelope.
- Verify API reference — every endpoint including profile CRUD.
- Voice biometrics API reference — enroll / challenge / verify shapes.