Send and verify one-time passcodes (OTPs) via Twilio Verify over SMS, RCS, voice, email, or WhatsApp. Covers creating a Verify Service, sending tokens, checking submitted codes, automatic WhatsApp-to-SMS fallback, and service configuration. TOTP is supported via the Factors API (a separate family from channel-based OTP). Use this skill to add phone or email verification or two-factor authentication to any application.
75
92%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Use Twilio Verify to manage the full OTP lifecycle: code generation, delivery, expiry, rate limiting, and Fraud Guard protection. Use the Programmable Messaging API to build your own OTP message infrastructure and access features such as SMS Pumping Protection.
| Twilio Verify | Programmable Messaging API | |
|---|---|---|
| Code generation + expiry | Built-in (10min default, configurable). Also supports custom codes. | Build yourself |
| Rate limiting | Built-in (per-phone, per-service) | Build yourself |
| Fraud protection | Fraud Guard (geo-permissions, rate anomaly) | SMS Pumping Protection |
| A2P registration | Exempt — no 10DLC needed | Required — must register campaign |
| Multi-channel | One API, change channel param (SMS/Voice/Email/WhatsApp/RCS) | Separate integration per channel |
| Cost | Per confirmed verification + channel fee | Per-message pricing + build cost |
| Delivery confirmation | Yes — via List Attempts or Events API | Yes (via StatusCallback) |
When Programmable Messaging is justified: You need full control over message content, custom delivery logic, or SMS Pumping Protection features. For standard OTP/2FA flows, use Verify.
Verify supports SMS, voice, email, WhatsApp, and RCS — only the channel parameter changes per delivery method. TOTP (authenticator apps) is supported via the Verify Factors API, a separate implementation from channel-based OTP.
twilio-account-setup
— Verify requires no separate product activation — just create a Service belowTWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKENVERIFY_SERVICE_SID (created in Quickstart step 1)
— See twilio-iam-auth-setup for credential setup and best practicespip install twilio / npm install twiliotwilio-whatsapp-manage-sendersStep 1 — Create a Verify Service (one-time)
Python
import os
from twilio.rest import Client
client = Client(os.environ["TWILIO_ACCOUNT_SID"], os.environ["TWILIO_AUTH_TOKEN"])
service = client.verify.v2.services.create(
friendly_name="My App Verification"
)
print(service.sid) # VAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx — save as VERIFY_SERVICE_SIDNode.js
const twilio = require("twilio");
const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
const service = await client.verify.v2.services.create({
friendlyName: "My App Verification",
});
console.log(service.sid); // VAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxStore the Service SID — reuse it for all verifications, do not recreate it each time.
Step 2 — Send a verification token
Python
verification = client.verify.v2 \
.services(os.environ["VERIFY_SERVICE_SID"]) \
.verifications \
.create(to="+15558675310", channel="sms")
print(verification.status) # pendingNode.js
const verification = await client.verify.v2
.services(process.env.VERIFY_SERVICE_SID)
.verifications.create({ to: "+15558675310", channel: "sms" });
console.log(verification.status); // pendingStep 3 — Check the submitted code
Python
check = client.verify.v2 \
.services(os.environ["VERIFY_SERVICE_SID"]) \
.verification_checks \
.create(to="+15558675310", code="123456")
if check.status == "approved":
print("Verified!")
else:
print("Invalid or expired code")Node.js
const check = await client.verify.v2
.services(process.env.VERIFY_SERVICE_SID)
.verificationChecks.create({ to: "+15558675310", code: "123456" });
if (check.status === "approved") {
console.log("Verified!");
} else {
console.log("Invalid or expired code");
}| Channel | channel value | Notes |
|---|---|---|
| SMS | sms | Default, widest coverage |
| Voice call | voice | Reads code aloud |
email | Use email address in to | |
whatsapp | Requires own WhatsApp sender (see below) | |
| RCS | rcs | Rich messaging, Android devices |
TOTP (authenticator apps): Supported via the Verify Factors API — a separate implementation from channel-based OTP. See Verify TOTP docs.
Change channel to "whatsapp" — the send/check flow is identical to SMS.
Requires: A registered production WhatsApp sender. As of March 2024, Twilio no longer provides a shared sender for Verify. See
twilio-whatsapp-manage-senders.
Python
verification = client.verify.v2 \
.services(os.environ["VERIFY_SERVICE_SID"]) \
.verifications \
.create(to="+15558675310", channel="whatsapp")Node.js
const verification = await client.verify.v2
.services(process.env.VERIFY_SERVICE_SID)
.verifications.create({ to: "+15558675310", channel: "whatsapp" });Python
verification = client.verify.v2 \
.services(os.environ["VERIFY_SERVICE_SID"]) \
.verifications \
.create(
to="+15558675310",
channel="whatsapp",
channel_configuration={
"whatsapp": {"enabled": True},
"sms": {"enabled": True} # falls back to SMS if WhatsApp undelivered
}
)Node.js
const verification = await client.verify.v2
.services(process.env.VERIFY_SERVICE_SID)
.verifications.create({
to: "+15558675310",
channel: "whatsapp",
channelConfiguration: {
whatsapp: { enabled: true },
sms: { enabled: true },
},
});With fallback enabled, your UI can say "a verification code was sent" without specifying the channel.
Python
service = client.verify.v2.services.create(
friendly_name="My App",
code_length=6, # 4–10 digits (default: 6)
lookup_enabled=True, # Validate number before sending
do_force_check_once=True, # Code can only be checked once
ttl=600, # Code expiry in seconds (default: 600)
)Node.js
const service = await client.verify.v2.services.create({
friendlyName: "My App",
codeLength: 6,
lookupEnabled: true,
doForceCheckOnce: true,
ttl: 600,
});| Status | Meaning |
|---|---|
approved | Code is correct |
pending | Code is wrong or not yet submitted |
expired | Code has expired (default TTL: 10 minutes) |
canceled | Verification was canceled |
Primary debugging tool: Console > Verify > Logs (per-Service). Shows every verification attempt, delivery status, channel used, and error codes. Check here first before writing custom monitoring code.
| Code | Meaning | Fix |
|---|---|---|
| 60200 | Invalid parameter | Check to format and channel value |
| 60202 | Max send attempts reached | Wait before retrying |
| 60203 | Max check attempts reached | Issue a new verification |
| 60212 | Service not found | Verify VERIFY_SERVICE_SID is correct |
| 60410 | Geo-permission not enabled | Enable country in Console |
Built-in protections (no custom code needed):
lookup_enabled=True)International OTP traffic warning: International numbers are high-risk for SMS pumping — fraudsters trigger OTPs to premium-rate destinations to generate revenue. Verify's Fraud Guard handles this automatically when enabled. If you're building custom OTP with Programmable Messaging instead, enable SMS Pumping Protection on your Messaging Service (see twilio-messaging-services). Always restrict geo-permissions to only countries where you have real users.
channel_configuration for WhatsApp→SMS only.verification_checks. Rate-limited: 60/min, 180/hr, 250/day.auto channel not universally available — Returns error 60200 on accounts without Fraud Guard enabled.channel: 'email' without a configured Mailer returns error 60217.status: "pending", not an error. You must check status === "approved" explicitly.approved, subsequent checks return 404.twilio-whatsapp-manage-senderstwilio-lookup-phone-intelligencetwilio-iam-auth-setup11c74d6
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.