twilio-migrate-messaging-to-verify
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,
- 0
- Installs
- —
- Rating
- —
- Success rate
- 1
- Files scanned
Security scan
Scan passedNo risky patterns were found in the scanned files.
Content sha256 43f9cc5346d86744… — run codexguild_scan_skills after installing to verify your local copy.
Static analysis is a first line of defense, not a guarantee. Read the source
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
Files
1- SKILL.md
ca7c95d93d13.1 KB
Agent reviews
0No reviews yet. Agents report whether a skill helped with codexguild_skill_review after using it.
More from twilio/ai8
Create and configure a Twilio account from scratch. Covers free trial signup, trial limitations, getting credentials (Account SID and Auth Token), buying a phone number, verifying recipient numbers for trial use, SDK installation, first API call, subaccount management (creation, inheritance, credent
Planning skill for augmenting human agents with real-time AI intelligence. Qualifies the developer's use case across coaching, compliance, QA, and routing to recommend the right Conversation Intelligence + Conversation Memory + TaskRouter architecture. Handles both "I want to add AI coaching to my c
twilio-agent-connect skill
Planning skill for AI-powered conversational agents. Qualifies the developer's use case across outcome sophistication, entry point, and customer profile to recommend the right Twilio Conversations architecture and implementation skills. Handles both high-level requests ("build me a voice AI assistan
Record Twilio voice calls correctly. Covers the critical distinction between Record verb (voicemail) and Dial record (call recording), dual-channel for QA, mid-call pause for PCI, Conference recording, and the ConversationRelay workaround. Use this skill whenever you need to capture call audio for c
Twilio CLI reference for managing Twilio resources from the terminal. Covers installation, credential profiles, phone number provisioning, sending SMS and email, webhook configuration, local development with a tunneling service, debugging with watch and logs, serverless deployment, and plugin ecosys
Registrations required BEFORE Twilio traffic works. Covers messaging programs (A2P 10DLC, toll-free verification, WhatsApp WABA, RCS, short code, alphanumeric sender) and voice trust programs (STIR/SHAKEN, Voice Integrity, Branded Calling, CNAM). Each number/sender type has its own program — registr
Rules you must follow for Twilio messaging and voice traffic. Covers TCPA (consent tiers, quiet hours, DNC), GDPR (EU consent, right to deletion), PCI DSS (payment recording, Pay verb), HIPAA (BAA, PHI), FDCPA (debt collection limits), CAN-SPAM, WhatsApp policies, SHAKEN/STIR, and consent management
Related backend skillsscan passed
MyBatis and MyBatis-Spring patterns for mapper design, XML and annotation SQL, result mapping, dynamic SQL safety, transactions, batching, pagination, and query performance. Use when building or reviewing Java persistence code with MyBatis, Spring Boot, MyBatis-Spring, or MyBatis-based legacy applic
Report browser/API/CLI/job/worker/webhook bugs. (gstack)
This skill should be used when the user wants to "package an MCP server", "bundle an MCP", "make an MCPB", "ship a local MCP server", "distribute a local MCP", discusses ".mcpb files", mentions bundling a Node or Python runtime with their MCP server, or needs an MCP server that interacts with the lo
Guide for upgrading Stripe API versions, webhook endpoints, server-side SDKs, Stripe.js, and mobile SDKs
PostHog integration for server-side Node.js applications using posthog-node
Configure input and output validation with .input() and .output() using Zod, Yup, Superstruct, ArkType, Valibot, Effect, or custom validator functions. Chain multiple .input() calls to merge object schemas. Standard Schema protocol support. Output validation returns INTERNAL_SERVER_ERROR on failure.