
Skill
twilio-migrate-messaging-to-verify
migrate Twilio messaging flows to Verify API
Description
Migrate an existing OTP / one-time-passcode flow from the Twilio Programmable Messaging API (self-built: generate code → send SMS → validate) to the Twilio Verify API (managed code generation, delivery, expiry, rate limiting, and Fraud Guard). Covers what Verify replaces, creating a Verify Service, replacing send + check logic, E.164 formatting with Lookup, bring-your-own custom codes, error-code translation, and a safe incremental cutover. Use when a developer is moving OTP/2FA delivery off Programmable Messaging onto Verify, or asks "should I use Verify instead of sending my own SMS codes?".
SKILL.md
Overview
If you built OTP on the Programmable Messaging API, you are maintaining code that Twilio Verify manages for you: random code generation, a database of codes with expiry, resend/rate-limit logic, and fraud filtering. Verify replaces that entire stack with two API calls — send a verification, check a code — and adds one-click Fraud Guard (SMS pumping protection), managed sender pools, and automatic A2P/compliance handling.
What you delete when you migrate:
| You built (Programmable Messaging) | Verify replaces it with |
|---|---|
| Buy/manage a sending number or Messaging Service | Managed sender pool (short/long code, toll-free, alpha ID) chosen per-country |
random.randint(...) code generation | Built-in generation (4–10 digits, default 6) |
DB table storing code, phone, expires_at | Stateless — Verify tracks the code and 10-min expiry |
| Custom resend throttling / attempt counting | Built-in rate limits (5 sends & 5 checks per number / 10 min) |
| SMS pumping / fraud filtering | Fraud Guard (geo-permissions, rate anomaly) — one click |
| A2P 10DLC campaign registration for the OTP number | Exempt — Verify traffic needs no 10DLC campaign |
| Per-channel integration (SMS, voice, WhatsApp…) | One API; change the channel param |
| Locale-specific message copy | Pre-screened templates in dozens of languages, auto-resolved by country |
This skill focuses on the migration. For full Verify API reference (all channels, service config, status values), see
twilio-verify-send-otp. To decide whether Verify fits before writing code, seetwilio-identity-verification-advisor.
When to stay on Programmable Messaging: you need full control of the message body/branding beyond what templates allow, custom delivery routing, or you are sending non-OTP transactional traffic. For standard OTP/2FA, migrate to Verify.
Prerequisites
- An existing Programmable Messaging OTP implementation (the thing you're replacing)
- Twilio account — New to Twilio? See
twilio-account-setup - Environment variables:
TWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKENVERIFY_SERVICE_SID(created in Step 1 below) — Seetwilio-iam-auth-setupfor credential setup
- SDK:
pip install twilio/npm install twilio(same SDK you already use) - Verify needs no separate product activation — just create a Service
Migration steps
The self-built pattern you're replacing does three things by hand: generate a random code, store it with an expiry, send it over a number you own — then later look up + compare + check expiry yourself. Verify collapses all of that into the two calls below. (The "What you delete" table above maps each piece to its Verify replacement.)
The Verify pattern
Step 1 — Create a Verify Service (one-time)
Do this once and reuse the SID. Create it in the Console or via API:
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="Acme") # your business name — shown in the SMS
print(service.sid) # VAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx — save in environment variable as VERIFY_SERVICE_SID
Node.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: "Acme" }); // your business name — shown in the SMS
console.log(service.sid); // VAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
friendlyNameappears in the default verification SMS (e.g. "Your Acme verification code is 123456"), so set it to your business name — not an internal label. Avoid embedding 5 or more digits in the name — it can trigger error 60200.
Step 2 — Send the verification (replaces your existing messaging logic)
This single verifications.create call replaces steps you did by hand:
Verify generates the code, tracks it with a 10-min expiry, and delivers it over a
managed sender — so whatever you used to produce the code, persist it, and send
the SMS can all go. A status of pending means the code is on its way.
Python
verification = client.verify.v2 \
.services(os.environ["VERIFY_SERVICE_SID"]) \
.verifications \
.create(to="+15558675310", channel="sms")
print(verification.status) # pending
Node.js
const verification = await client.verify.v2
.services(process.env.VERIFY_SERVICE_SID)
.verifications.create({ to: "+15558675310", channel: "sms" });
console.log(verification.status); // pending
No code generation, no DB write, no owned number. Switch channel by changing
channel to "call", "whatsapp", or "email" (also "sna" and "auto").
RCS isn't a channel value — Verify upgrades an sms verification to RCS where
the device supports it and falls back to SMS automatically.
Step 3 — Check the code (replaces your existing validation logic)
Python
check = client.verify.v2 \
.services(os.environ["VERIFY_SERVICE_SID"]) \
.verification_checks \
.create(to="+15558675310", code="123456")
if check.status == "approved":
grant_access()
else:
reject() # "pending" = wrong/not-yet-submitted, "expired" = past TTL
Node.js
const check = await client.verify.v2
.services(process.env.VERIFY_SERVICE_SID)
.verificationChecks.create({ to: "+15558675310", code: "123456" });
if (check.status === "approved") grantAccess();
else reject();
A wrong code does not throw — it returns
status: "pending". You must teststatus === "approved"explicitly. After approval the verification is single-use; re-checking it returns 404. Delete your OTP database table once traffic is fully on Verify.
Key patterns
Format numbers as E.164 (Verify is strict)
Programmable Messaging tolerated loose formats; Verify requires E.164
(+15558675310). If your DB stores national-format numbers, normalize them
before sending or you'll get error 60200. Use the Lookup API to convert user
input to E.164 and capture the ISO country code — useful for fraud allow/block
lists. See twilio-lookup-phone-intelligence.
Keep your own codes (bring-your-own OTP)
If you can't rip out your existing generation logic on day one, pass your code
via custom_code so Verify still handles delivery, templates, and localization.
Python
verification = client.verify.v2 \
.services(os.environ["VERIFY_SERVICE_SID"]) \
.verifications \
.create(to="+15558675310", channel="sms", custom_code="123456")
Caveats — read before relying on this:
- Enable it per-Service first — it is not on by default. Set
CustomCodeEnabled=truewhen creating/updating the Service via the API (services.update(custom_code_enabled=True)), or toggle "Enable Custom Verification Code" in the Console (Verify → Services → your Service → General). - Do not pass a custom message body. Send only the code via
custom_code; Verify maps it into an approved template. Custom message text returns error 60243. - You are billed per attempted send (not per completed verification), and custom-code flows tend to generate more sends than checks (abandoned/re-requested codes) — expect higher cost than standard Verify.
- Report the outcome so Twilio can keep deliverability high. Verify's route-intelligence engine uses whether a code gets validated as a signal — it automatically retries different carriers/routes when codes go unvalidated or delivery underperforms. Tell Verify the result by updating the verification's status to
approvedso Twilio can keep optimizing routing:
Python
client.verify.v2 \
.services(os.environ["VERIFY_SERVICE_SID"]) \
.verifications(verification.sid) \
.update(status="approved") # after your own code check passes
Treat custom codes as a transition aid, not the end state — migrating to Verify-generated codes removes all these caveats.
Translate your error handling
Verify uses different error codes than Messaging. Map them during migration:
| Situation | Verify error | Handle by |
|---|---|---|
| Malformed / non-E.164 number, bad param | 60200 | Fix to format (use Lookup) |
| Too many sends to a number | 60203 | Back off; surface "try again later" |
| Too many check attempts | 60202 | Issue a new verification |
| Service SID wrong | 60212 | Check VERIFY_SERVICE_SID |
| Country not enabled | 60410 | Enable geo-permission in Console |
| Custom message passed | 60243 | Use custom_code only, not a body |
Full Verify error handling and built-in protections: twilio-verify-send-otp.
Rate limits & fraud — delete your custom throttling
Verify enforces, per phone per service, out of the box: 5 sends / 10 min and
5 check attempts per verification. Codes live 10 minutes and are
immutable within that window (a re-request inside 10 min returns the same
code). This replaces hand-rolled resend counters. Need different limits? Define
custom Rate Limits / Buckets
on the Service. Enable Fraud Guard in the Console to retire your SMS-pumping
defenses. See twilio-security-hardening for the pumping-attack background.
Safe incremental cutover
Don't flip 100% of traffic at once:
- Stand up Verify in parallel. Keep the Messaging path live; route a small % of new OTP requests (or one region / internal users) through Verify.
- Dual-run the check. If a user was issued a legacy code, validate against your DB; if issued via Verify, use
verification_checks. Track which system issued each code so checks hit the right validator. - Drain, then delete. Once no legacy codes remain within their 10-min TTL, stop issuing them, ramp Verify to 100%, then decommission the OTP number/Messaging Service and drop the OTP DB table.
- Reuse one Verify Service across the app — do not create a Service per request.
For testing without tripping rate limits during the cutover, see Twilio's Verify testing docs.
CANNOT
- Cannot retrieve the code Verify generated — never returned by any API (by design for security). If your old flow logged or displayed the code, that stops working.
- Cannot reuse your owned number as the Verify sender by default — Verify picks from managed sender pools per country. (You can bring your own number/Messaging Service for specific channels/WhatsApp, but that's not the default.)
- Cannot pass a raw custom message body — the
customMessageparam is deprecated; raw text → error 60243. To customize the copy, use an approved template (TemplateSid) instead. custom_codehas strings attached — must be enabled per-Service (Console → Verify → Services → General), bills per attempt (higher cost than standard Verify), and mandates updating Verification Status manually.- Cannot change the 10-minute token TTL yourself — to change the default (adjustable between 2 minutes and 24 hours), you must contact Twilio Support to set it on your Service.
- Cannot re-check an approved verification — single-use; a second check returns 404.
- Cannot send to non-E.164 numbers — normalize first (Lookup).
- No push webhook on completion — poll
verification_checks(rate-limited: 60/min, 180/hr, 250/day) or use the Verify Events API. - Cannot skip WhatsApp sender setup — as of March 1, 2024 there is no shared Verify WhatsApp sender; bring your own. See
twilio-whatsapp-manage-senders. - Verify traffic is A2P-exempt, but your remaining non-OTP Messaging is not — keep 10DLC registration for any other traffic on that number.
Next Steps
- Full Verify API reference (all channels, service config):
twilio-verify-send-otp - Decide product fit / architecture first:
twilio-identity-verification-advisor - Normalize numbers to E.164 + fraud signals:
twilio-lookup-phone-intelligence - Bring a WhatsApp sender for WhatsApp OTP:
twilio-whatsapp-manage-senders - Retire SMS-pumping defenses safely:
twilio-security-hardening - Credential setup:
twilio-iam-auth-setup - Source blog post: Migrate from Programmable Messaging to Verify
More skills from the ai repository
View all 57 skillstwilio-account-setup
configure Twilio accounts and credentials
Aug 1API DevelopmentCommunicationsSMStwilio-agent-augmentation-architect
augment human agents with AI intelligence
Jul 17AgentsAICoachingQA +1twilio-agent-connect
connect AI agents to Twilio channels
Jul 17AgentsAPI DevelopmentCommunicationsTwiliotwilio-ai-agent-architect
plan Twilio conversational AI agents
Jul 17AgentsArchitectureTwiliotwilio-call-recordings
record and manage Twilio voice calls
Jul 17AudioComplianceQATwiliotwilio-cli-reference
manage Twilio resources via CLI
Jul 17CLILocal DevelopmentOperationsTwilio
More from Twilio
View publishertwilio-compliance-onboarding
manage Twilio messaging and voice compliance
ai
Jul 17ComplianceMessagingSMSTwilio +1twilio-compliance-traffic
ensure compliance for Twilio messaging traffic
ai
Jul 17ComplianceMessagingRegulatory ComplianceSecurity +1twilio-conference-calls
build multi-party calls with Twilio Conference
ai
Jul 17AudioCommunicationsMeetingsTwiliotwilio-content-template-builder
create and send message templates with Twilio
ai
Jul 17EmailMessagingSMSTemplates +1twilio-conversation-intelligence
build conversation intelligence pipelines
ai
Jul 17AgentsAnalyticsMonitoringNLP +1twilio-conversation-memory
manage conversation memory with Twilio
ai
Jul 17AgentsMemoryTwilio