Skip to main content

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; for the fallback-chain deep dive, see Verify profiles: fallback chains and advanced factors.
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 (we cover it there, not here).

1. What a profile owns

A profile groups six concerns under one id: 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: 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 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 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: 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; sign-verification specifics for Verify webhooks are in 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. 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).
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.