Skip to main content
Verify handles the mechanics of phone verification — code generation, delivery, expiry, attempt limits, and brute-force lockout — so your backend only makes two HTTP calls: send the code and check the code the user types. Optional webhook events let you react asynchronously, and a profile bundles your channel strategy, TTL, and template choices into one reusable object. This tutorial walks the whole flow: create a profile, send an SMS OTP, check the code, subscribe to verification.approved / verification.failed, then layer on a fallback chain and an optional fraud-score pre-check.

Before you start

  • An Orbit API key (dv_live_sk_… or dv_test_sk_…). Key-level rate limits (20 /send calls and 60 /check calls per minute per key) are ample for this tutorial.
  • The channels you plan to deliver on connected under Settings → Channels if you use WhatsApp, voice, or email.

1. Create a verification profile

A profile pins the code length, TTL, attempt cap, hourly rate, and (optionally) a channel fallback chain so every send inherits those rules. Without a profile you get platform defaults: 6-digit code, 600-second TTL, 3 attempts, single channel.
Response (201):
Store id — you pass it as profile_id on every send that should follow these rules.

2. Send the OTP

The user taps “send code” in your app; your backend posts the send:
Response (201):
Save verification_id against your user session — you’ll need it for the check call. expires_at is the row TTL; after it, checks fail with EXPIRED_TOKEN (410).

3. Check the code

Your UI collects the digits, then your backend validates them:
On a correct code:
Treat your user as verified. A wrong code returns 422 VALIDATION_ERROR with attempts_remaining in the details block — surface that to the user. The full status and error matrix is in Verify integration without our SDK.

4. Subscribe to verification events (optional)

Instead of (or in addition to) the synchronous check response, subscribe to webhooks so post-verification side effects (mint a session, unlock an account) run on an idempotent worker. Configure the endpoint under Settings → Webhooks and pick the verification.* events — primarily verification.approved, verification.failed, and verification.fallback_exhausted. A minimal receiver (Node, no framework):
Webhooks deliver at-least-once with retries, so handle each event idempotently. Signature verification recipe: Webhook verification; event payload shapes: Webhook events reference and the dedicated Webhook signatures guide.

5. Add a fallback chain

SMS alone fails for users in poor coverage or with full-connectivity blockers. Make the profile’s channels array ordered: try each in turn. Update your profile:
A voice entry requires the templates.voice script containing {{code}}. Fallback advances asynchronously on provider rejection or timeout, re-delivering the same code on each channel — so a recipient who reads the SMS and then hears the voice call gets one identical code. Watch verification.fallback_triggered for each hop or verification.fallback_exhausted for a hard failure. Full profile mechanics: Verify profiles and fallback chains.

6. Optional: fraud-score gate

Before dispatching an OTP you can dip the number for risk. POST /verify/fraud-score returns a 0–100 score with low/medium/high banding and an advisory allow/step_up/block. POST /verify/fraud-gate is the composite operator fusion (SIM-swap, port-event, silent-network-auth when available) returning allow/review/deny. Both are rate-limited to 20 dips per minute per tenant. Use the gate to refuse-or-step-up risky numbers before you spend an OTP; when a dip fails in flight the verification lands in the review bucket rather than a fabricated clean verdict. The same fraud surface is visible in the dashboard console along with code-attempt logs and fallback timelines — see the Verify console guide and the full API at Verify API reference.

7. Common errors

Next steps