> ## 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.

# Verify in 30 minutes

> Stand up a production-ready OTP verification flow in about 30 minutes: create a profile, send and check codes with plain HTTP, subscribe to the verification events, and add a multi-channel fallback chain.

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.

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/verify/profiles" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "App login OTP",
    "channels": ["sms"],
    "codeLength": 6,
    "expirySeconds": 600,
    "maxAttempts": 3,
    "rateLimitPerHour": 5
  }'
```

Response (201):

```json theme={null}
{
  "data": {
    "id": "vprof_7a1c0e2b…",
    "name": "App login OTP",
    "channels": ["sms"],
    "status": "active"
  }
}
```

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:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/verify/send" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "channel": "sms",
    "profile_id": "vprof_7a1c0e2b…"
  }'
```

Response (201):

```json theme={null}
{
  "data": {
    "verification_id": "vrf_3f1c0b2a8e4d4f7a9c2b1e6d5a4c3b2a",
    "status": "pending",
    "channel": "sms",
    "expires_at": "2026-09-12T14:10:00.000Z"
  }
}
```

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:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/verify/check" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "verification_id": "vrf_3f1c0b2a8e4d4f7a9c2b1e6d5a4c3b2a",
    "code": "482910"
  }'
```

On a correct code:

```json theme={null}
{
  "data": {
    "verification_id": "vrf_3f1c0b2a8e4d4f7a9c2b1e6d5a4c3b2a",
    "status": "approved",
    "attempts_remaining": 0
  }
}
```

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](/guides/verify-no-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):

```js theme={null}
import http from 'node:http';
import crypto from 'node:crypto';

const server = http.createServer((req, res) => {
  let body = '';
  req.on('data', (c) => (body += c));
  req.on('end', () => {
    const sig = req.headers['x-orbit-signature'];
    // Sig format: t=<unix>,v1=<hex>; HMAC-SHA256 of "<t>.<raw_body>" keyed by
    // your endpoint signing secret. Verify before trusting the payload.
    if (verifySignature(body, sig, process.env.ORBIT_WEBHOOK_SECRET)) {
      const event = JSON.parse(body);
      if (event.type === 'verification.approved') {
        markVerified(event.data.verification_id);
      }
    }
    res.end('ok');
  });
});
server.listen(3001);
```

Webhooks deliver at-least-once with retries, so handle each event idempotently. Signature verification recipe: [Webhook verification](/guides/security#webhook-verification); event payload shapes: [Webhook events reference](/reference/webhook-events#verification-events) and the dedicated [Webhook signatures guide](/guides/verify-webhook-signatures).

## 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:

```bash theme={null}
curl -X PATCH "https://api.orbit.devotel.io/api/v1/verify/profiles/vprof_7a1c0e2b…" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channels": ["sms", "voice", "whatsapp"],
    "templates": {
      "voice": "Your {{app_name}} verification code is {{code}}. I repeat, {{code}}."
    }
  }'
```

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](/guides/verify-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](/guides/verify-console) and the full API at [Verify API reference](/api-reference/endpoints/verify).

## 7. Common errors

| Error                       | HTTP | Cause                                            | Fix                                                        |
| --------------------------- | ---- | ------------------------------------------------ | ---------------------------------------------------------- |
| Profile not found           | 404  | `profile_id` missing or deleted                  | Send without `profile_id` (defaults) or create the profile |
| `EXPIRED_TOKEN`             | 410  | Check arrived after `expires_at`                 | Create a new verification                                  |
| `VALIDATION_ERROR`          | 422  | Wrong code (`attempts_remaining` in details)     | Surface remaining tries to the user                        |
| Duplicate `verification_id` | —    | A second `/send` creates a new row               | Use `/verify/:id/resend` for re-dispatch on the same row   |
| `RATE_LIMIT_EXCEEDED`       | 429  | Key, per-recipient, or brute-force layer tripped | Honour `Retry-After`; recipient-level defaults at 5/hour   |

## Next steps

* [Verify overview](/verify/overview) — channel catalog and factor suite (TOTP, push, passkeys).
* [Verify API reference](/api-reference/endpoints/verify) — every endpoint shape.
