Twilio logo

Skill

twilio-migrate-messaging-to-verify

migrate Twilio messaging flows to Verify API

Covers Auth SMS Migration Messaging Twilio

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 ServiceManaged sender pool (short/long code, toll-free, alpha ID) chosen per-country
random.randint(...) code generationBuilt-in generation (4–10 digits, default 6)
DB table storing code, phone, expires_atStateless — Verify tracks the code and 10-min expiry
Custom resend throttling / attempt countingBuilt-in rate limits (5 sends & 5 checks per number / 10 min)
SMS pumping / fraud filteringFraud Guard (geo-permissions, rate anomaly) — one click
A2P 10DLC campaign registration for the OTP numberExempt — Verify traffic needs no 10DLC campaign
Per-channel integration (SMS, voice, WhatsApp…)One API; change the channel param
Locale-specific message copyPre-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, see twilio-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_SID
    • TWILIO_AUTH_TOKEN
    • VERIFY_SERVICE_SID (created in Step 1 below) — See twilio-iam-auth-setup for 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

friendlyName appears 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 test status === "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=true when 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 approved so 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:

SituationVerify errorHandle by
Malformed / non-E.164 number, bad param60200Fix to format (use Lookup)
Too many sends to a number60203Back off; surface "try again later"
Too many check attempts60202Issue a new verification
Service SID wrong60212Check VERIFY_SERVICE_SID
Country not enabled60410Enable geo-permission in Console
Custom message passed60243Use 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:

  1. Stand up Verify in parallel. Keep the Messaging path live; route a small % of new OTP requests (or one region / internal users) through Verify.
  2. 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.
  3. 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.
  4. 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 customMessage param is deprecated; raw text → error 60243. To customize the copy, use an approved template (TemplateSid) instead.
  • custom_code has 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

© 2026 YourAI.tools. Every skill from an identity-verified publisher.

Independent catalog. Not affiliated with, endorsed by, or sponsored by Anthropic or any listed publisher. All trademarks belong to their respective owners.