Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
alphaonedev avatar

Twilio Verify

  • 51 installs
  • 6 repo stars
  • Updated March 13, 2026
  • alphaonedev/openclaw-graph

twilio-verify is a skill for implementing Twilio Verify (V2) in production: SMS/voice/email/TOTP OTP, phone verification, Fraud Guard, and Silent Network Authentication.

About

This skill helps engineers implement and operate Twilio Verify (V2) in production for phone and identity verification. A developer uses it to build a send-then-check OTP pipeline over SMS, voice, email, or TOTP, and to add Fraud Guard risk scoring and Silent Network Authentication. It covers rate limiting, idempotency, E.164 normalization, and webhook-driven status handling.

  • 2FA and OTP via SMS, voice, email, and TOTP with Twilio Verify V2
  • Verify Fraud Guard risk scoring and Silent Network Authentication
  • Rate limiting, idempotency, and abuse prevention for verification flows

Twilio Verify by the numbers

  • 51 all-time installs (skills.sh)
  • Ranked #3,229 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
  • Data as of Jul 28, 2026 (Skillselion catalog sync)
At a glance

twilio-verify capabilities & compatibility

Capabilities
twilio sms · twilio lookup · twilio conversations
Works with
redis
Use cases
api development · security audit
Pricing
Bring your own API key
From the docs

What twilio-verify says it does

SMS/voice/email OTP, TOTP, custom channels, phone verification, Verify Fraud Guard (risk scoring + blocking), and Silent Network Authentication (SNA) where available.
SKILL.md
Twilio expects phone numbers in **E.164** format: `+14155552671`.
SKILL.md
npx skills add https://github.com/alphaonedev/openclaw-graph --skill twilio-verify

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs51
repo stars6
Last updatedMarch 13, 2026
Repositoryalphaonedev/openclaw-graph

What it does

Build a 2FA/OTP verification pipeline with Twilio Verify V2 across SMS, voice, email, and TOTP with fraud controls.

Who is it for?

Building a reliable send-check-enforce verification pipeline with rate limits and fraud controls.

When should I use this skill?

When implementing 2FA, OTP, phone verification, or fraud-guarded verification with Twilio Verify in production.

By the numbers

  • Supports six verification channels including SMS, call, email, and TOTP
  • Tested with twilio npm 4.22.0 and Python 9.0.5

Files

SKILL.mdMarkdownGitHub ↗

twilio-verify

Purpose

Enable OpenClaw to implement and operate Twilio Verify (V2) in production: SMS/voice/email OTP, TOTP, custom channels, phone verification, Verify Fraud Guard (risk scoring + blocking), and Silent Network Authentication (SNA) where available. This skill focuses on:

  • Building a reliable verification pipeline (send → check → enforce) with rate limits, fraud controls, and observability.
  • Integrating Verify with Programmable Messaging/Voice, SendGrid, and webhook-driven status/telemetry.
  • Handling real Twilio failure modes (carrier filtering, invalid E.164, auth errors, rate limits) with deterministic remediation.
  • Operating at scale: cost controls, regional routing, idempotency, and abuse prevention.

---

Prerequisites

Accounts & Twilio resources

  • Twilio account with:
  • Account SID (AC...)
  • Auth Token (or API Key + Secret)
  • A Verify Service SID (VA...) created in Twilio Console → Verify → Services
  • If using SMS/Voice:
  • A verified Messaging Service (recommended) or phone number(s)
  • If US A2P: 10DLC registration completed for your brand/campaign (or use toll-free/short code as appropriate)
  • If using email channel:
  • SendGrid account + verified sender domain, or Twilio Verify Email channel configuration (depending on your setup)
  • If using SNA:
  • SNA availability depends on region/carrier and Twilio enablement; confirm in Console and with Twilio support.

Local tooling (exact versions)

  • Node.js 20.11.1 (LTS) or 18.19.1 (LTS)
  • Python 3.12.2 (if using Python examples)
  • Twilio helper libraries:
  • twilio npm package 4.22.0
  • twilio Python package 9.0.5
  • HTTP tooling:
  • curl 8.5.0
  • jq 1.7
  • Optional (recommended):
  • ngrok 3.13.1 for webhook testing
  • openssl 3.0.13 for signature verification utilities

Auth setup (recommended patterns)

Prefer API Key auth over Auth Token for server-side apps.

  • Create API Key in Twilio Console → Account → API keys & tokens:
  • API Key SID (SK...)
  • API Key Secret (store once)
  • Store secrets in a secret manager (AWS Secrets Manager, GCP Secret Manager, Vault). Do not commit to repo.

Environment variables expected by examples:

  • TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • TWILIO_AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (if using Auth Token)
  • TWILIO_API_KEY_SID=SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • TWILIO_API_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • TWILIO_VERIFY_SERVICE_SID=VAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

---

Core Concepts

Verify Service

A Verify Service (VA...) is the policy boundary for:

  • Channels enabled (sms, call, email, whatsapp, push, custom)
  • Code length, locale templates, TTL, rate limits
  • Fraud Guard configuration (risk thresholds, blocking)
  • Webhooks (status callbacks, events)

Treat a Verify Service as an environment-scoped resource:

  • VA_prod... for production
  • VA_staging... for staging
  • Separate services for different products/tenants only if policy differs materially.

Verification vs Verification Check

  • Verification: sending a challenge (OTP) to a destination (phone/email) via a channel.
  • Verification Check: validating the user-provided code (or other factor) against the verification attempt.

Your application should: 1. Create verification (send) 2. Accept user input 3. Create verification check (verify) 4. Enforce outcome (issue session token, mark phone verified, etc.)

Channels

Common channels:

  • sms: OTP via SMS
  • call: OTP via voice call (TwiML-driven by Twilio)
  • email: OTP via email (Verify email channel or custom)
  • totp: time-based one-time password (app-based)
  • whatsapp: WhatsApp OTP (requires WhatsApp enablement)
  • custom: your own delivery mechanism (push, in-app, etc.)

Channel selection should be policy-driven:

  • Default to sms
  • Offer call fallback for deliverability
  • Offer email for account recovery or when phone is unavailable
  • Offer totp for high-assurance accounts

E.164 normalization

Twilio expects phone numbers in E.164 format: +14155552671.

Do not accept raw user input directly. Normalize and validate:

  • Use libphonenumber (Node: google-libphonenumber or libphonenumber-js)
  • Store canonical E.164 in DB
  • Reject ambiguous numbers early

Rate limiting & abuse controls

Verify has built-in rate limiting, but you should also implement:

  • Per-IP and per-identity throttles (Redis token bucket)
  • Device fingerprinting / risk scoring
  • Cooldowns after failed attempts
  • CAPTCHA gating for suspicious traffic

Fraud Guard (Verify Fraud Guard)

Fraud Guard helps detect:

  • SIM swap risk
  • High-risk destinations
  • Traffic anomalies

Integrate Fraud Guard decisions into your auth flow:

  • Block high-risk verifications
  • Step-up to stronger factor (TOTP) if medium risk
  • Log risk signals for incident response

Webhooks & eventing

Use webhooks for:

  • Verification status events
  • Delivery outcomes (for messaging/voice)
  • Audit trails and analytics

Design webhooks as:

  • Idempotent (dedupe by event SID)
  • Authenticated (Twilio signature validation)
  • Retry-safe (Twilio retries on non-2xx)

Silent Network Authentication (SNA)

SNA verifies a user’s phone number via carrier network signals without OTP entry (where supported). Treat it as:

  • A step-up or frictionless verification path
  • Not universally available; implement fallback to OTP
  • Subject to carrier/region constraints and privacy requirements

---

Installation & Setup

Official Python SDK — Verify

Repository: https://github.com/twilio/twilio-python PyPI: pip install twilio · Supported: Python 3.7–3.13

from twilio.rest import Client
client = Client()

SERVICE_SID = os.environ["TWILIO_VERIFY_SERVICE_SID"]

# Start verification (SMS / WhatsApp / email / TOTP)
verification = client.verify.v2.services(SERVICE_SID) \
    .verifications.create(to="+15558675309", channel="sms")
print(verification.status)

# Check code
check = client.verify.v2.services(SERVICE_SID) \
    .verification_checks.create(to="+15558675309", code="123456")
print(check.status)  # "approved" | "pending"

Source: twilio/twilio-python — verify

Ubuntu 22.04 LTS (x86_64)

sudo apt-get update
sudo apt-get install -y curl jq ca-certificates gnupg

# Node.js 20.x (NodeSource)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs

node -v   # v20.11.1 (or later 20.x)
npm -v

# Python 3.12 (deadsnakes PPA)
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:deadsnakes/ppa
sudo apt-get update
sudo apt-get install -y python3.12 python3.12-venv python3.12-dev
python3.12 --version

Fedora 39 (x86_64)

sudo dnf install -y curl jq nodejs python3.12 python3.12-devel
node -v
python3.12 --version

macOS 14 (Sonoma) — Intel & Apple Silicon

brew update
brew install node@20 python@3.12 jq curl openssl@3

# Ensure PATH includes brew Node/Python
node -v
python3.12 --version

Project dependencies (Node.js)

mkdir -p verify-service && cd verify-service
npm init -y
npm install twilio@4.22.0 express@4.18.3 pino@9.0.0 zod@3.22.4
npm install --save-dev tsx@4.7.1 typescript@5.3.3 @types/express@4.17.21

Project dependencies (Python)

mkdir -p verify-service-py && cd verify-service-py
python3.12 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip==24.0
pip install twilio==9.0.5 fastapi==0.109.2 uvicorn==0.27.1 pydantic==2.6.1

Environment configuration

Create a local env file (do not commit):

  • Node: /etc/openclaw/twilio-verify.env (production) or ./.env (local)
  • Python: same

Example (local):

cat > ./.env <<'EOF'
TWILIO_ACCOUNT_SID=AC2f7c2c6b2d2f4a1b9a0b3c1d2e3f4a5
TWILIO_API_KEY_SID=YOUR_API_KEY_SID
TWILIO_API_KEY_SECRET=9b2c3d4e5f60718293a4b5c6d7e8f9a0
TWILIO_VERIFY_SERVICE_SID=VA0a1b2c3d4e5f60718293a4b5c6d7e8f
TWILIO_AUTH_TOKEN=use_api_key_in_prod_if_possible
APP_ENV=local
EOF

Load it:

set -a
source ./.env
set +a

---

Key Capabilities

Send OTP via SMS/Voice/Email (Verify V2)

Core operation: create a Verification.

  • SMS:
  • Best default for consumer sign-in
  • Watch for carrier filtering and A2P compliance
  • Voice:
  • Fallback when SMS fails
  • Ensure user experience: language/voice, repeat code, DTMF handling if needed
  • Email:
  • Useful for account recovery or when phone is not available
  • Ensure SPF/DKIM/DMARC alignment if using SendGrid/custom email

Node example (send):

// src/send.ts
import twilio from "twilio";

const accountSid = process.env.TWILIO_ACCOUNT_SID!;
const apiKeySid = process.env.TWILIO_API_KEY_SID!;
const apiKeySecret = process.env.TWILIO_API_KEY_SECRET!;
const verifyServiceSid = process.env.TWILIO_VERIFY_SERVICE_SID!;

const client = twilio(apiKeySid, apiKeySecret, { accountSid });

export async function sendOtp(to: string, channel: "sms" | "call" | "email") {
  const verification = await client.verify.v2
    .services(verifyServiceSid)
    .verifications.create({
      to,
      channel,
      locale: "en",
    });

  return {
    sid: verification.sid,
    status: verification.status, // "pending"
    to: verification.to,
    channel: verification.channel,
  };
}

Check OTP (Verification Check)

Create a Verification Check with the user-provided code.

Node example (check):

// src/check.ts
import twilio from "twilio";

const accountSid = process.env.TWILIO_ACCOUNT_SID!;
const apiKeySid = process.env.TWILIO_API_KEY_SID!;
const apiKeySecret = process.env.TWILIO_API_KEY_SECRET!;
const verifyServiceSid = process.env.TWILIO_VERIFY_SERVICE_SID!;

const client = twilio(apiKeySid, apiKeySecret, { accountSid });

export async function checkOtp(to: string, code: string) {
  const check = await client.verify.v2
    .services(verifyServiceSid)
    .verificationChecks.create({ to, code });

  return {
    sid: check.sid,
    status: check.status, // "approved" or "pending"/"canceled"
    valid: check.valid,
  };
}

Enforcement rule (typical):

  • Accept only status === "approved" and valid === true
  • On failure, increment local counters and apply cooldowns

TOTP enrollment and verification

Use Verify TOTP for app-based codes. Typical flow: 1. Create a TOTP factor (enrollment) 2. Display QR code / secret to user 3. Verify initial code 4. Store factor SID and bind to user

Note: Twilio Verify TOTP APIs are part of Verify V2 “Entities/Factors” model. Ensure your account has access and you’re using the correct endpoints.

Operational guidance:

  • Treat factor SIDs as secrets (they’re identifiers, but still sensitive)
  • Allow multiple factors per user (device migration)
  • Provide recovery codes outside Twilio (your system)

Custom channels (email/push/in-app)

Use Verify “custom” channel when you deliver the code yourself but want Twilio to manage:

  • Code generation
  • TTL
  • Attempt limits
  • Verification checks

Pattern:

  • Request verification with channel=custom
  • Twilio returns a code (or you fetch it via API depending on configuration)
  • Deliver via your channel (push, in-app)
  • Verify via Verification Check

This is useful when:

  • You have an existing push infrastructure
  • You want consistent policy enforcement across channels

Fraud Guard integration

Use Fraud Guard signals to:

  • Block verification attempts to high-risk destinations
  • Require step-up (TOTP) for medium risk
  • Alert on spikes per ASN/country

Implementation pattern:

  • On “send verification” request:
  • Evaluate risk (Twilio + your own)
  • If blocked: return 403 with generic message
  • Else: proceed

Rate limiting and throttling

Combine:

  • Twilio Verify service rate limits
  • Application-level throttles

Recommended minimums:

  • Per IP: 5 sends / 10 minutes
  • Per destination: 3 sends / 10 minutes
  • Per identity (user id): 5 checks / 10 minutes
  • Global circuit breaker on Twilio 5xx spikes

Webhook-driven observability

Use:

  • Verify event webhooks (where configured)
  • Messaging status callbacks for SMS delivery outcomes (if using Messaging)
  • Voice status callbacks for call outcomes

Store:

  • verification SID
  • destination hash (HMAC)
  • channel
  • status transitions
  • error codes

---

Command Reference

This section assumes direct REST usage via curl and helper library usage. Twilio does not provide an official “twilio verify” CLI with full parity; use REST calls or helper SDKs.

REST: Create a Verification (send OTP)

Endpoint:

  • POST https://verify.twilio.com/v2/Services/{ServiceSid}/Verifications

Auth:

  • Basic auth with API Key SID/Secret (preferred) or Account SID/Auth Token

Flags/fields (important):

  • To (string): destination (+14155552671 or email)
  • Channel (string): sms|call|email|whatsapp|custom
  • Locale (string): e.g. en, es, fr
  • ChannelConfiguration.* (object): channel-specific config (varies)
  • CustomFriendlyName (string): label for logs/UX
  • RateLimits.* (object): service-level overrides (if enabled)
  • RiskCheck / Fraud Guard fields (if enabled; account-dependent)

Example (SMS):

curl -sS -X POST "https://verify.twilio.com/v2/Services/${TWILIO_VERIFY_SERVICE_SID}/Verifications" \
  -u "${TWILIO_API_KEY_SID}:${TWILIO_API_KEY_SECRET}" \
  --data-urlencode "To=+14155552671" \
  --data-urlencode "Channel=sms" \
  --data-urlencode "Locale=en"

Example (Voice call):

curl -sS -X POST "https://verify.twilio.com/v2/Services/${TWILIO_VERIFY_SERVICE_SID}/Verifications" \
  -u "${TWILIO_API_KEY_SID}:${TWILIO_API_KEY_SECRET}" \
  --data-urlencode "To=+14155552671" \
  --data-urlencode "Channel=call" \
  --data-urlencode "Locale=en"

Example (Email):

curl -sS -X POST "https://verify.twilio.com/v2/Services/${TWILIO_VERIFY_SERVICE_SID}/Verifications" \
  -u "${TWILIO_API_KEY_SID}:${TWILIO_API_KEY_SECRET}" \
  --data-urlencode "To=alice@example.com" \
  --data-urlencode "Channel=email" \
  --data-urlencode "Locale=en"

REST: Create a Verification Check (validate OTP)

Endpoint:

  • POST https://verify.twilio.com/v2/Services/{ServiceSid}/VerificationCheck

Fields:

  • To (string): same destination used for send
  • Code (string): user-provided OTP
  • VerificationSid (string): optional in some flows; prefer To+Code unless you store SID

Example:

curl -sS -X POST "https://verify.twilio.com/v2/Services/${TWILIO_VERIFY_SERVICE_SID}/VerificationCheck" \
  -u "${TWILIO_API_KEY_SID}:${TWILIO_API_KEY_SECRET}" \
  --data-urlencode "To=+14155552671" \
  --data-urlencode "Code=123456"

REST: List Verifications (audit/debug)

Endpoint:

  • GET https://verify.twilio.com/v2/Services/{ServiceSid}/Verifications

Query params (common):

  • To (string)
  • Status (string): pending|approved|canceled
  • Channel (string)
  • DateCreated (date filter; Twilio-style)
  • PageSize (int): up to 1000 depending on endpoint

Example:

curl -sS "https://verify.twilio.com/v2/Services/${TWILIO_VERIFY_SERVICE_SID}/Verifications?To=%2B14155552671&PageSize=50" \
  -u "${TWILIO_API_KEY_SID}:${TWILIO_API_KEY_SECRET}" | jq .

REST: Fetch a Verification by SID

Endpoint:

  • GET https://verify.twilio.com/v2/Services/{ServiceSid}/Verifications/{VerificationSid}
curl -sS "https://verify.twilio.com/v2/Services/${TWILIO_VERIFY_SERVICE_SID}/Verifications/VExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -u "${TWILIO_API_KEY_SID}:${TWILIO_API_KEY_SECRET}" | jq .

REST: Cancel a Verification (invalidate)

Endpoint:

  • POST https://verify.twilio.com/v2/Services/{ServiceSid}/Verifications/{VerificationSid} with Status=canceled
curl -sS -X POST "https://verify.twilio.com/v2/Services/${TWILIO_VERIFY_SERVICE_SID}/Verifications/VExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -u "${TWILIO_API_KEY_SID}:${TWILIO_API_KEY_SECRET}" \
  --data-urlencode "Status=canceled"

REST: Verify Service configuration (read)

Endpoint:

  • GET https://verify.twilio.com/v2/Services/{ServiceSid}
curl -sS "https://verify.twilio.com/v2/Services/${TWILIO_VERIFY_SERVICE_SID}" \
  -u "${TWILIO_API_KEY_SID}:${TWILIO_API_KEY_SECRET}" | jq .

Twilio Node helper library: client initialization options

twilio(apiKeySid, apiKeySecret, { accountSid, region, edge, logLevel })

Important options:

  • accountSid: required when using API Key auth
  • region: e.g. us1, ie1, au1 (data residency/latency)
  • edge: e.g. ashburn, dublin (latency optimization)
  • logLevel: debug|info|warn|error (avoid debug in prod)

Example:

import twilio from "twilio";
const client = twilio(process.env.TWILIO_API_KEY_SID!, process.env.TWILIO_API_KEY_SECRET!, {
  accountSid: process.env.TWILIO_ACCOUNT_SID!,
  region: "us1",
  edge: "ashburn",
});

---

Configuration Reference

OpenClaw skill config (example)

Path:

  • /etc/openclaw/skills/twilio/twilio-verify.toml
# /etc/openclaw/skills/twilio/twilio-verify.toml
[twilio]
account_sid = "AC2f7c2c6b2d2f4a1b9a0b3c1d2e3f4a5"
auth_mode = "api_key" # "api_key" | "auth_token"
api_key_sid_env = "TWILIO_API_KEY_SID"
api_key_secret_env = "TWILIO_API_KEY_SECRET"
auth_token_env = "TWILIO_AUTH_TOKEN"
region = "us1"
edge = "ashburn"

[verify]
service_sid = "VA0a1b2c3d4e5f60718293a4b5c6d7e8f"
default_channel = "sms"
fallback_channels = ["call", "email"]
default_locale = "en"
code_ttl_seconds = 600

[rate_limits]
# App-level throttles (in addition to Twilio)
send_per_ip_per_10m = 5
send_per_to_per_10m = 3
check_per_identity_per_10m = 5
cooldown_seconds_after_failed_check = 60

[fraud_guard]
enabled = true
block_on_high_risk = true
step_up_on_medium_risk = true
step_up_channel = "totp"

[logging]
redact_fields = ["to", "email", "code", "auth_token", "api_key_secret"]
log_level = "info"

Node service config (example)

Path:

  • ./config/verify.config.json
{
  "twilio": {
    "region": "us1",
    "edge": "ashburn"
  },
  "verify": {
    "serviceSid": "VA0a1b2c3d4e5f60718293a4b5c6d7e8f",
    "defaultLocale": "en",
    "channels": ["sms", "call", "email"]
  },
  "security": {
    "hmacKeyEnv": "VERIFY_DESTINATION_HMAC_KEY",
    "webhookAuthTokenEnv": "TWILIO_AUTH_TOKEN"
  }
}

systemd unit (production)

Path:

  • /etc/systemd/system/openclaw-verify.service
[Unit]
Description=OpenClaw Verify Gateway
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=openclaw
Group=openclaw
EnvironmentFile=/etc/openclaw/twilio-verify.env
WorkingDirectory=/opt/openclaw/verify-gateway
ExecStart=/usr/bin/node /opt/openclaw/verify-gateway/dist/server.js
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/openclaw /var/log/openclaw
AmbientCapabilities=
CapabilityBoundingSet=
LockPersonality=true
MemoryDenyWriteExecute=true

[Install]
WantedBy=multi-user.target

---

Integration Patterns

Compose with Programmable Messaging status callbacks

Even when using Verify, you may need delivery telemetry. Pattern:

  • Use Verify for OTP generation and checking
  • Use Messaging status callbacks for delivery outcomes (if your setup routes via Messaging Service)
  • Correlate by to hash + timestamp window + verification SID

Pipeline: 1. POST /verify/send → Twilio Verify creates verification 2. Twilio sends SMS 3. Messaging status callback hits /webhooks/sms-status 4. Store MessageSid, MessageStatus, ErrorCode (e.g., 30003) 5. If repeated failures, auto-switch to voice/email

Compose with Voice IVR fallback

If SMS fails:

  • Offer voice call OTP
  • If voice fails, route to agent or require TOTP

If you already have IVR state machines:

  • Keep Verify call OTP separate from IVR calls
  • Use IVR only for support flows; Verify call is optimized for OTP

Compose with SendGrid for custom email channel

If you need branded email beyond Verify templates:

  • Use Verify custom channel to generate code
  • Send email via SendGrid dynamic templates
  • Verify via Verification Check

SendGrid dynamic template example (Handlebars):

{
  "personalizations": [
    {
      "to": [{ "email": "alice@example.com" }],
      "dynamic_template_data": {
        "code": "123456",
        "ttl_minutes": 10
      }
    }
  ],
  "from": { "email": "no-reply@example.com", "name": "Example Security" },
  "template_id": "d-13b8f94f2b2a4c4f9a8d0a1b2c3d4e5f"
}

CI/CD: smoke test Verify in staging

In pipeline:

  • Deploy staging
  • Run a smoke test that:
  • Sends OTP to a test phone (or uses test credentials)
  • Checks OTP using a controlled channel (Twilio test credentials do not send real SMS)
  • Gate production deploy on:
  • Twilio API auth success
  • Verify service reachable
  • Webhook endpoint signature verification passes

Data model integration

Store:

  • user_id
  • destination_e164 (encrypted at rest)
  • destination_hash (HMAC for logs)
  • verify_service_sid
  • last_verification_sid
  • verified_at
  • failed_attempts
  • cooldown_until

Do not store OTP codes.

---

Error Handling & Troubleshooting

Handle Twilio errors by code, not by string matching, but include exact messages for operator recognition.

1) 20003 — Authentication Error

Message: Twilio could not authenticate the request. Please check your credentials.

Root causes:

  • Wrong API Key Secret/Auth Token
  • Using API Key without accountSid in SDK init
  • Clock skew rarely affects auth but can affect signature validation

Fix:

  • Verify env vars and secret manager values
  • For Node SDK with API Key: pass { accountSid }
  • Rotate API key if leaked

2) 20429 — Too Many Requests

Message: Rate limit exceeded

Root causes:

  • Verify service rate limits hit
  • Burst traffic (bot attack)
  • Repeated resend loops in client

Fix:

  • Implement app-level throttles and exponential backoff
  • Add resend cooldown UI (e.g., 30–60s)
  • Consider separate Verify Services for distinct traffic classes only if policy differs

3) 21211 — Invalid 'To' Phone Number

Message: The 'To' number +1415555 is not a valid phone number.

Root causes:

  • Not E.164
  • User typed local format without country
  • Bad parsing/normalization

Fix:

  • Normalize with libphonenumber
  • Require country selection or infer from user profile
  • Reject early with actionable UX

4) 30003 — Unreachable destination handset / carrier filtering

Message (Messaging): Unreachable destination handset

Root causes:

  • Carrier filtering (A2P issues, content filtering)
  • Number inactive/out of coverage
  • Wrong destination type (landline for SMS)

Fix:

  • Offer voice fallback
  • Ensure 10DLC compliance and correct sender type (toll-free/short code)
  • Use Messaging Service with geo-matching and proper sender pools

5) 60200 — Invalid parameter (Verify)

Message: Invalid parameter: Channel

Root causes:

  • Unsupported channel string
  • Channel not enabled for the Verify Service

Fix:

  • Validate channel enum in API layer
  • Ensure service configuration includes the channel
  • Use separate service if policy differs

6) 60203 — Max check attempts reached / verification blocked

Message: Max check attempts reached

Root causes:

  • User repeatedly entered wrong code
  • Attack on a destination

Fix:

  • Enforce cooldown and require resend after lockout
  • Add bot mitigation
  • Alert on spikes per destination hash

7) 60202 — Verification expired

Message: Verification expired

Root causes:

  • User waited beyond TTL
  • Delivery delays (carrier)
  • Client clock confusion (UX)

Fix:

  • Increase TTL if justified (tradeoff: security)
  • Improve UX: show countdown and resend option
  • Prefer voice fallback for delayed SMS

8) Webhook signature validation failure

Typical log: Error: Twilio Request Validation Failed.

Root causes:

  • Using wrong Auth Token for validation
  • URL mismatch (ngrok URL changed, missing query string)
  • Reverse proxy rewriting host/path

Fix:

  • Validate against the exact public URL Twilio calls
  • Preserve original URL in proxy (X-Forwarded-Host, X-Forwarded-Proto)
  • Keep Auth Token consistent; rotate carefully

9) 11200 — HTTP retrieval failure (Voice/TwiML)

Message (Voice debugger): HTTP retrieval failure

Root causes:

  • Twilio cannot reach your webhook (firewall, DNS)
  • TLS misconfiguration
  • Slow response > timeout

Fix:

  • Ensure public HTTPS endpoint
  • Reduce latency; respond within a few seconds
  • Add health checks and multi-region ingress

10) 21610 — STOP / opt-out (Messaging)

Message: Attempt to send to unsubscribed recipient

Root causes:

  • User replied STOP to your sender
  • You are reusing a sender pool without opt-out awareness

Fix:

  • Respect opt-out; do not attempt further SMS
  • Offer voice/email/TOTP alternatives
  • Maintain suppression list keyed by destination

---

Security Hardening

Secrets management

  • Store Twilio API Key Secret/Auth Token in a secret manager.
  • Rotate API keys quarterly or after incidents.
  • Use least privilege: separate API keys per environment.

Webhook validation (mandatory)

Validate Twilio signatures for any inbound webhook.

Node example:

import twilio from "twilio";
import type { Request, Response } from "express";

export function validateTwilioWebhook(req: Request, res: Response, next: Function) {
  const authToken = process.env.TWILIO_AUTH_TOKEN!;
  const signature = req.header("X-Twilio-Signature") || "";
  const url = `${req.protocol}://${req.get("host")}${req.originalUrl}`;

  const isValid = twilio.validateRequest(authToken, signature, url, req.body);
  if (!isValid) return res.status(403).send("Forbidden");
  next();
}

Operational notes:

  • If behind a proxy, set app.set('trust proxy', true) and reconstruct URL using forwarded headers.
  • Ensure body parsing preserves raw body if required by your framework; some setups need raw body for validation.

PII handling

  • Treat phone numbers and emails as PII.
  • Log only:
  • HMAC(destination) with a rotation-capable key
  • last 2 digits for debugging (optional)
  • Encrypt destination at rest (KMS envelope encryption).

CIS-aligned host hardening (high-level pointers)

  • CIS Ubuntu Linux 22.04 LTS Benchmark:
  • Disable password SSH auth; enforce key-based
  • Enable automatic security updates
  • Restrict outbound egress from app hosts to Twilio endpoints only where feasible
  • systemd sandboxing (see unit file above)
  • Run as non-root, read-only filesystem where possible

Abuse prevention

  • Require proof-of-work / CAPTCHA for suspicious send attempts.
  • Block disposable email domains for email channel (policy-dependent).
  • Add ASN/country anomaly detection.

---

Performance Tuning

Reduce Twilio API latency with region/edge

Set region and edge in SDK init.

Expected impact:

  • Typical p50 improvement: 30–80ms depending on proximity
  • p95 improvement: 50–150ms in cross-region deployments

Measure:

  • Instrument sendOtp and checkOtp durations
  • Compare before/after with same traffic

Connection reuse and timeouts

  • Use keep-alive HTTP agents (Node) to reduce TLS handshake overhead.
  • Set sane timeouts:
  • connect timeout: 2s
  • request timeout: 5s (send), 5s (check)
  • Implement retries only for safe failure modes (network errors, 5xx). Do not retry on 4xx.

Cache normalization results

Phone parsing can be expensive at scale.

  • Cache E.164 normalization per raw input for short TTL (e.g., 10 minutes) keyed by (raw, defaultCountry).

Avoid resend loops

Client UX:

  • Disable resend button for 30 seconds
  • Show countdown
  • Backoff on repeated failures

This reduces:

  • Twilio costs
  • 20429 rate limits
  • Carrier filtering risk

---

Advanced Topics

Idempotency strategy

Twilio Verify “send” is not inherently idempotent across repeated calls. Implement app-level idempotency:

  • Compute key: sha256(user_id + destination + channel + floor(now/30s))
  • Store in Redis with TTL 60s
  • If key exists, return existing verification SID/status

This prevents accidental double-sends from:

  • mobile retries
  • double-clicks
  • network timeouts

Multi-channel fallback policy

Implement deterministic fallback: 1. SMS 2. If SMS delivery fails with 30003 or no delivery within 20s → Voice 3. If voice fails → Email or TOTP enrollment prompt

Do not automatically fallback without user consent in some jurisdictions; ensure compliance.

Handling landlines and VoIP

  • Some numbers are not SMS-capable.
  • Use a carrier lookup (Twilio Lookup API) to detect line type:
  • If landline: skip SMS, offer voice/email
  • If VoIP: consider higher fraud risk; step-up factor

Internationalization

  • Set Locale based on user preference.
  • Ensure templates exist for target locales.
  • For voice, ensure correct language/voice selection (if using custom voice flows).

Verify + account linking

When verifying phone for account linking:

  • Require authenticated session before allowing phone change verification
  • Enforce re-authentication for sensitive changes
  • Prevent “phone takeover” by requiring existing factor confirmation

SNA fallback design

If SNA is enabled:

  • Attempt SNA first for eligible devices/networks
  • If unavailable/failed:
  • fallback to SMS/voice
  • Log SNA eligibility and failure reasons for tuning

---

Usage Examples

Scenario 1: Sign-in OTP via SMS with resend cooldown (Node + Express)

// src/server.ts
import express from "express";
import twilio from "twilio";
import { z } from "zod";
import pino from "pino";

const log = pino({ level: process.env.LOG_LEVEL || "info" });
const app = express();
app.use(express.json());

const client = twilio(process.env.TWILIO_API_KEY_SID!, process.env.TWILIO_API_KEY_SECRET!, {
  accountSid: process.env.TWILIO_ACCOUNT_SID!,
  region: "us1",
  edge: "ashburn",
});

const serviceSid = process.env.TWILIO_VERIFY_SERVICE_SID!;

const SendSchema = z.object({
  to: z.string().min(5),
  channel: z.enum(["sms", "call", "email"]).default("sms"),
});

const CheckSchema = z.object({
  to: z.string().min(5),
  code: z.string().min(4).max(10),
});

app.post("/verify/send", async (req, res) => {
  const { to, channel } = SendSchema.parse(req.body);

  // TODO: enforce app-level rate limits here (Redis token bucket)
  const v = await client.verify.v2.services(serviceSid).verifications.create({
    to,
    channel,
    locale: "en",
  });

  log.info({ verificationSid: v.sid, channel: v.channel }, "verify_send");
  res.json({ sid: v.sid, status: v.status });
});

app.post("/verify/check", async (req, res) => {
  const { to, code } = CheckSchema.parse(req.body);

  const c = await client.verify.v2.services(serviceSid).verificationChecks.create({ to, code });

  log.info({ checkSid: c.sid, status: c.status, valid: c.valid }, "verify_check");

  if (c.status === "approved" && c.valid) {
    // Issue session token, mark verified, etc.
    return res.json({ ok: true });
  }
  return res.status(401).json({ ok: false });
});

app.listen(3000, () => log.info("listening on :3000"));

Run:

npx tsx src/server.ts
curl -sS -X POST http://localhost:3000/verify/send -H 'content-type: application/json' \
  -d '{"to":"+14155552671","channel":"sms"}' | jq .

Scenario 2: Voice fallback after SMS failure (policy-driven)

Pseudo-logic:

type DeliverySignal = { smsFailed: boolean; smsTimedOut: boolean };

function chooseChannel(signal: DeliverySignal) {
  if (signal.smsFailed || signal.smsTimedOut) return "call";
  return "sms";
}

Operationally:

  • Use Messaging status callbacks to detect failed with 30003
  • Or time out after 20 seconds without delivered (not always available for SMS)
  • Offer user a “Call me instead” option

Scenario 3: Email OTP using custom channel + SendGrid template

1) Request custom verification:

curl -sS -X POST "https://verify.twilio.com/v2/Services/${TWILIO_VERIFY_SERVICE_SID}/Verifications" \
  -u "${TWILIO_API_KEY_SID}:${TWILIO_API_KEY_SECRET}" \
  --data-urlencode "To=alice@example.com" \
  --data-urlencode "Channel=custom" | jq .

2) Deliver code via SendGrid (your app sends email). 3) Check code via Verify VerificationCheck.

Scenario 4: TOTP enrollment for high-risk accounts

Flow:

  • User signs in with password
  • Risk engine flags medium/high risk
  • Require TOTP enrollment:
  • Create factor
  • Verify initial code
  • Store factor SID
  • On future sign-ins, require TOTP check

Key production detail:

  • Provide recovery path (support + identity proofing)
  • Allow multiple devices

Scenario 5: Phone verification for profile changes (step-up)

When user changes phone number:

  • Require existing session + re-auth
  • Send OTP to new number
  • Only after approval:
  • update phone in DB
  • mark verified_at
  • Prevent swapping to already-verified number owned by another account unless policy allows

Scenario 6: Webhook endpoint with signature validation and idempotency

import express from "express";
import twilio from "twilio";
import crypto from "crypto";

const app = express();

// For some frameworks you may need raw body; adjust accordingly.
app.use(express.urlencoded({ extended: false }));

const seen = new Set<string>(); // replace with Redis in prod

app.post("/webhooks/verify-events", (req, res) => {
  const authToken = process.env.TWILIO_AUTH_TOKEN!;
  const signature = req.header("X-Twilio-Signature") || "";
  const url = `${req.protocol}://${req.get("host")}${req.originalUrl}`;

  const ok = twilio.validateRequest(authToken, signature, url, req.body);
  if (!ok) return res.status(403).send("Forbidden");

  const eventSid = req.body.Sid || req.body.EventSid || "";
  const dedupeKey = crypto.createHash("sha256").update(eventSid).digest("hex");
  if (seen.has(dedupeKey)) return res.status(200).send("ok");
  seen.add(dedupeKey);

  // Persist event, update metrics, etc.
  return res.status(200).send("ok");
});

---

Quick Reference

TaskCommand / APIKey flags/fields
Send OTPPOST /v2/Services/{VA}/VerificationsTo, Channel, Locale
Check OTPPOST /v2/Services/{VA}/VerificationCheckTo, Code
List verificationsGET /v2/Services/{VA}/VerificationsTo, Status, Channel, PageSize
Fetch verificationGET /v2/Services/{VA}/Verifications/{VE}n/a
Cancel verificationPOST /v2/Services/{VA}/Verifications/{VE}Status=canceled
Auth (preferred)API KeySK... + secret + accountSid
Common errorsTwilio codes20003, 20429, 21211, 30003, 60202, 60203
Webhook securitySignature validationX-Twilio-Signature, exact URL

---

Graph Relationships

DEPENDS_ON

  • twilio-core-auth (Account SID + API Key/Auth Token handling)
  • twilio-webhooks (signature validation, retry/idempotency patterns)
  • pii-handling (redaction, encryption at rest)
  • rate-limiting (Redis token bucket / leaky bucket)

COMPOSES

  • twilio-messaging (delivery telemetry, STOP handling, 10DLC considerations)
  • twilio-voice (voice fallback, call status callbacks)
  • sendgrid-transactional (custom email channel delivery, bounce handling)
  • studio-flows (optional orchestration for complex verification journeys)

SIMILAR_TO

  • auth-otp-generic (OTP flows without Twilio-managed policy)
  • firebase-phone-auth (phone verification managed by another provider)
  • okta-verify (factor-based verification with enterprise IAM)

Related skills

FAQ

What channels does it support?

SMS, voice call, email, TOTP, WhatsApp, and custom delivery channels.

What is Fraud Guard?

Verify Fraud Guard detects SIM swap risk, high-risk destinations, and traffic anomalies, letting you block or step up verifications.

Backend & APIsbackendintegrations

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.