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

# Verification configuration: templates, expiry, attempts, rate limits, webhooks

> Everything the Verify → Configuration surface controls for one verification profile — per-channel templates, code TTL, attempt budget, rate limits, webhook delivery, and channel fallback order.

# Verification configuration

The **Verify → Configuration** surface (dashboard **Outbound → Verify → Configuration**) owns the verification profile — the tenant-owned settings record every `POST /verify/send` can bind by `profile_id`. One profile carries the templates, code TTL, attempt budget, rate limits, webhook URL, and channel order for one verification product, so a send inherits consistent behaviour without passing options in the body.

The dashboard page is a profile list — create, edit, search, activate/deactivate, and delete profiles. This page is a reference for what each group of settings does and how it interacts with the rest of the platform. For the wizard walkthrough, see [Manage verification profiles from the Configuration console](/guides/verify-configuration-console); for the fallback-chain deep dive, see [Verify profiles: fallback chains and advanced factors](/guides/verify-fallback-chains).

<Note>
  **Tenant-owned controls only.** Every control on this page is configured by your organization's owners, admins, and developers. Template edits can be routed through an org-level two-officer gate — see [OTP approvals](/verify/otp-approvals) (we cover it there, not here).
</Note>

## 1. What a profile owns

A profile groups six concerns under one id:

| Concern | Profile field(s) |
| - | - |
| Delivery | Ordered channel chain `channels[]` — index 0 is the primary, the rest are the async fallback order |
| Code shape | `codeLength` (4, 6, or 8 digits) |
| Expiry | `expirySeconds` (60–3600 seconds) |
| Attempts | `maxAttempts` (1–10 per verification) |
| Rate limits | `rateLimitPerHour` (1–1000 sends per recipient per hour) plus fraud velocity caps |
| Webhooks | One `webhookUrl` the profile's verification events deliver on |

Fields left at their defaults (600-second TTL even for direct sends, 3 attempts, the platform hourly cap) apply until you override them on a profile and pass its `profile_id`. An `inactive` profile is blocked for new sends but keeps its history for `/check`.

## 2. Template configuration

Templates are a **per-channel body override map** on the profile. Set a body for the channels you want to brand; leave a channel's body empty and the server-side default applies.

**The `{{code}}` placeholder carries the OTP.** A template you save for `sms`, `email`, `voice`, `viber`, `telegram`, or `silent` **must** contain `{{code}}` (whitespace inside the braces, e.g. `{{ code }}`, is fine). The save path rejects a literal body without it — otherwise recipients would receive a message with no verification code in it. These placeholders resolve at send time:

| Placeholder | Resolves to |
| - | - |
| `{{code}}` | The OTP digits |
| `{{app_name}}` | Your brand name |
| `{{expiry_minutes}}` | The profile TTL expressed in minutes (use this, not a literal `{{minutes}}`) |
| `{{expiry}}` | The absolute expiry timestamp |
| `{{magic_link_url}}` | The signed single-use link (magic-link channel) |

**Per-channel shapes:**

* **SMS / email / voice / Viber / Telegram** — free-text body; `{{code}}` required when set. The `voice` body is what the TTS prompt reads aloud.
* **WhatsApp** — holds the *name* of a Meta-approved authentication template, picked through a dedicated chooser in the dashboard. The OTP lives inside the approved template on Meta's side, so the free-text `{{code}}` rule is exempt.
* **Flashcall / SNA / TOTP / push / magic\_link / backup\_code** — factor-style channels that deliver no literal code text. Their template fields are accepted for forward-compat (e.g. the notification body around a push prompt) but are exempt from the `{{code}}` requirement.

When the same profile runs a fallback chain, each chain hop renders that channel's template — so templates cascade through the chain the same way the channel order does. (See [Message cascade groups](/concepts/message-cascade-groups) for how platforms group sends under a cascade.) Channels sharing the profile are not grouped as one cascade; each channel message is its own record.

## 3. Expiry and attempts

**Code TTL.** `expirySeconds` is bounded at 60–3600 seconds (1 minute – 1 hour). The default form value is 300s; a direct send with no profile expires at the platform default of 600s. There is **no expiry parameter on `POST /verify/send`** — the TTL is profile-only, because the fallback engine and per-channel templates also live on the profile. After the TTL the code is dead: `/check` returns an expired state and a new send is required.

**Retry budget.** `maxAttempts` (1–10, default 3) is the number of `/check` calls a verification accepts. Each `/check` consumes one attempt whether the code matches or not. Two accounting rules to design around:

1. A `/check` against an **expired** verification still consumes an attempt — the attempt counter is independent of the TTL.
2. When the attempt budget hits `maxAttempts` the verification moves to a terminal `failed` state and `verification.failed` fires (CAS-guarded, exactly once). There is no separate lock-out flag — the terminal state **is** the lock-out: further `/check` calls on that verification id are rejected, and the user must request a new code.

## 4. Rate limits

Verify has two distinct rate layers, both tenant-configurable on the profile, both evaluated per recipient before any code is minted:

* **Per-destination attempt cap — `rateLimitPerHour`** (1–1000, default 5). A hard ceiling on OTP sends to one recipient per rolling hour; breach returns `429 VERIFY_RECIPIENT_RATE_LIMITED` per recipient, not per batch. Raise it above the platform default for high-resend signup flows (e.g. B2C onboarding that re-prompts aggressively).
* **Account-side velocity** — the profile's fraud caps `fraud.velocityPerHour` (1–100) and `fraud.velocityPerDay` (1–1000) run *on top of* the recipient cap, counting that tenant's sends to a recipient across the wider window. Use it to stay under your own budget even when the recipient ceiling was raised.

**Suppression interaction.** A recipient who previously opted out lands in the message-suppression store, and a suppression record still blocks a verify send when the OTP would ride the suppressed channel — waterfall order is suppression → fraud gate → recipient cap → velocity. Because suppression is checked *before* the caps, an opted-out recipient never consumes an hourly slot; the send short-circuits at the gate instead of returning a rate-limit. See [Message suppression model](/concepts/message-suppression-model) for what suppression does and does not block — a suppressed `sms` record does not force a fallback hop by itself, the fallback chain only advances on timeout/failure.

Two additional send-path layers worth knowing when you budget: the global resend cooldown (`VERIFY_RESEND_COOLDOWN`, a 30-second `SET NX` guard per recipient, run before the hourly counter so a burst does not eat hourly slots) and endpoint-level request ceilings (`verify-send` 20/min, `verify-bulk` 2/min, `verify-check` 60/min per tenant).

## 5. Webhook events

Save one `webhookUrl` per profile (HTTPS, or HTTP on localhost) and every lifecycle event from sends bound by that profile delivers there. A verification lifecycle can emit:

| Event | When |
| - | - |
| `verification.sent` | The code was handed to the provider successfully |
| `verification.checked` | Any `/check` attempt landed (success or not) — drive fraud scoring from the stream |
| `verification.approved` | A `/check` call matched the code — CAS-guarded, exactly once per verification |
| `verification.failed` | `maxAttempts` exhausted with no match, or the verification moved to a terminal unsuccessful state |
| `verification.fallback_triggered` | The async fallback engine advanced to the next channel |
| `verification.fallback_exhausted` | Every channel in the chain was tried with no approval |

A SIM-swap pre-flight block is the exception: the send returns a synchronous `403 SIM_SWAP_DETECTED` and **no row is created**, so no webhook fires — wire step-up to the response, not to the stream. Payload shape, signatures, and retry semantics live in [Webhook events and payloads](/webhooks/event-payloads); sign-verification specifics for Verify webhooks are in [Verify webhook signatures](/guides/verify-webhook-signatures).

## 6. Channel order

`channels[]` is an ordered chain (1–4 channels): index 0 is the primary send, and the remaining entries are tried in order by the fallback engine when the primary times out or fails. The dashboard wizard renders that chain as a drag-ordered list with explicit move-up/move-down buttons, so the order you save is the order the engine walks.

The typical progression rides hardest-to-carry-channel first and the friendliest fallback last — SMS first, voice next, email last — but the order is yours to set. For how the platform prioritizes that traffic when several profiles run concurrently, see [Message priority and traffic lanes](/concepts/message-priority-traffic-lanes). Channel-level timeout and per-channel attempts inside the chain come from `fallback_config` on the send (or the profile's saved fallback config): `channel_timeout_seconds` 10–600 (default 60) and `max_attempts_per_channel` 1–3 (default 1).

<Tip>
  **Configure chains end-to-end, not per-send.** Set `channels[]`, the fallback timeouts, templates per channel, and the webhook URL on the profile once; then have your sends pass only `profile_id`. Per-send `channels[]` and `fallback_config` overrides still work, but the profile is the auditable, reproducible form.
</Tip>

## Related

* [Verify overview](/verify/overview) — channel capabilities, send/check details, and profile field-level differences
* [Configuration console guide](/guides/verify-configuration-console) — wizard walkthrough, card grid, and edit flows
* [Fallback chains guide](/guides/verify-fallback-chains) — chains, fallback engine, and advanced factors
* [Webhook event payloads](/webhooks/event-payloads) — envelope shape, signatures, retry semantics
* [Message cascade groups](/concepts/message-cascade-groups) · [Message suppression model](/concepts/message-suppression-model) · [Message priority and traffic lanes](/concepts/message-priority-traffic-lanes)
