
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)
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
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.
Twilio expects phone numbers in **E.164** format: `+14155552671`.
npx skills add https://github.com/alphaonedev/openclaw-graph --skill twilio-verifyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 51 |
|---|---|
| repo stars | ★ 6 |
| Last updated | March 13, 2026 |
| Repository | alphaonedev/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
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:
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)
- 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: 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 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 --versionFedora 39 (x86_64)
sudo dnf install -y curl jq nodejs python3.12 python3.12-devel
node -v
python3.12 --versionmacOS 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 --versionProject 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.21Project 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.1Environment 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
EOFLoad 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: 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 (+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: 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
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: 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
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
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
| 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)
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.