twilio-verify
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
- Account SID (
- 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:
twilionpm package 4.22.0twilioPython package 9.0.5
- HTTP tooling:
curl8.5.0jq1.7
- Optional (recommended):
ngrok3.13.1 for webhook testingopenssl3.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)
- API Key SID (
- 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=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxTWILIO_AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(if using Auth Token)TWILIO_API_KEY_SID=SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxTWILIO_API_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxTWILIO_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 productionVA_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 SMScall: 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
callfallback for deliverability - Offer
emailfor account recovery or when phone is unavailable - Offer
totpfor 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-libphonenumberorlibphonenumber-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"andvalid === 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:
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 (+14155552671or email)Channel(string):sms|call|email|whatsapp|customLocale(string): e.g.en,es,frChannelConfiguration.*(object): channel-specific config (varies)CustomFriendlyName(string): label for logs/UXRateLimits.*(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 sendCode(string): user-provided OTPVerificationSid(string): optional in some flows; preferTo+Codeunless 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|canceledChannel(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}withStatus=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 authregion: 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
tohash + 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
customchannel 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_iddestination_e164(encrypted at rest)destination_hash(HMAC for logs)verify_service_sidlast_verification_sidverified_atfailed_attemptscooldown_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
accountSidin 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
sendOtpandcheckOtpdurations - 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
30003or 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
Localebased 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
failedwith30003 - 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)