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:
-
Create verification (send)
-
Accept user input
-
Create verification check (verify)
-
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:
-
Create a TOTP factor (enrollment)
-
Display QR code / secret to user
-
Verify initial code
-
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:
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:
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:
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:
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:
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:
-
POST /verify/send → Twilio Verify creates verification
-
Twilio sends SMS
-
Messaging status callback hits /webhooks/sms-status
-
Store MessageSid , MessageStatus , ErrorCode (e.g., 30003 )
-
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.
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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:
-
SMS
-
If SMS delivery fails with 30003 or no delivery within 20s → Voice
-
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
- 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 .
-
Deliver code via SendGrid (your app sends email).
-
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
Task Command / API Key flags/fields
Send OTP POST /v2/Services/{VA}/Verifications
To , Channel , Locale
Check OTP POST /v2/Services/{VA}/VerificationCheck
To , Code
List verifications GET /v2/Services/{VA}/Verifications
To , Status , Channel , PageSize
Fetch verification GET /v2/Services/{VA}/Verifications/{VE}
n/a
Cancel verification POST /v2/Services/{VA}/Verifications/{VE}
Status=canceled
Auth (preferred) API Key SK...
- secret + accountSid
Common errors Twilio codes 20003 , 20429 , 21211 , 30003 , 60202 , 60203
Webhook security Signature validation X-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)