Documentation
Sign inGet started

Migrate SMS from Twilio

This page maps Twilio's Programmable Messaging API, Messaging Services, and status callbacks to Bird. Follow the main migration guide in order and use these mappings for steps 3, 4, and 5.
Two differences shape the whole port. Twilio's POST /2010-04-01/Accounts/{AccountSid}/Messages.json takes form-encoded PascalCase parameters authenticated with your Account SID and Auth Token; POST /v1/sms/messages takes JSON authenticated with a bearer API key against your regional host. And a Twilio Messaging Service bundles a sender pool, opt-out handling, and callback URLs into one object; Bird splits those across senders, keyword rules, and webhook subscriptions, so there is nothing to recreate as a service.

Hand this to your agent

Paste this into Claude Code, Cursor, or Codex. The agent works through this page against your own repository, using whichever Bird surface it already has: the MCP server if one is connected, the CLI if it is installed and signed in.
Ejemplo de código
I am moving an SMS integration from Twilio to Bird. Route through it with me.
1. Check what you already have before setting anything up. If Bird's MCP server is connected, use its tools. If the Bird CLI is installed and signed in, use that. Either one is enough, and every step below is an action you take with whichever you have. Only if neither is present, follow https://bird.com/docs/ai/set-up-your-agent.md to set one up and sign me in. Every Bird docs page serves Markdown at its own URL with `.md` appended, so fetch that rather than the HTML.
2. Read https://bird.com/docs/guides/sms/migrate/twilio.md for the field, status and opt-out mapping, and https://bird.com/docs/guides/sms/migrate.md for the order the steps go in.
3. Find and list my Twilio usage in this repository before you change anything: the Messages.json call sites, the numbers they send from, and the StatusCallback URLs they register.
4. Then take only the paths that apply to me.
   - If I send to US long codes: read https://bird.com/docs/guides/sms/10dlc.md and work through the brand and campaign registration. Read the requirements first. A registration attempt is chargeable and a rejected brand is charged again on resubmit, so show me what you are about to submit and wait for me to say yes before you submit anything.
   - If I send with an alphanumeric or other registered sender ID: read the sender section of https://bird.com/docs/guides/sms/migrate.md and register it wherever the destination country requires it.
   - Carry over my opt-outs following https://bird.com/docs/guides/sms/opt-outs-and-keywords.md.
   - Ask me which Twilio numbers I want to keep, then open a Bird support ticket for the port, listing those numbers and saying they port from Twilio. Route it to a person rather than an automated answer. Give me the ticket ID. The port itself runs on Bird support's schedule, so plan the cutover around it.
5. Stop and ask me wherever a step needs a decision, and finish by telling me what is left that only a person can do.

Map the send call

What it doesTwilioBird
RecipientToto (one per request)
SenderFrom or MessagingServiceSidfrom
BodyBodytext
Content templateContentSid + ContentVariablestemplate + template.parameters
Intent(none)category, required on free text
Filterable labels(none)tags: {name, value} pairs
Round-trip contextyour own store, keyed by SIDmetadata: arbitrary JSON, echoed on every event
Delivery reportsStatusCallbacka workspace webhook subscribed to sms.*
TransliterationSmartEncodedoptions.smart_encoding (default false)
Safe retries(none on Messages)Idempotency-Key header
SchedulingScheduleType + SendAtno equivalent: scheduled_at is rejected
MediaMediaUrlno equivalent: media_urls is rejected
ValidityValidityPeriodno equivalent: validity_period is rejected
Link shorteningShortenUrlsno equivalent
The three rejected fields are reserved and answer 422 SMSUnsupportedFeature. Keep scheduling and media handling where they are for now.
Porting notes:
  • A Messaging Service SID becomes a plain sender value. Twilio resolves the sender pool, sticky sender, and geomatch behind the SID. Bird takes the sender itself in from, so pick the sender per send, or use a template send, which selects a valid sender for the destination and rejects from.
  • A character limit becomes a segment cap. The lengths work out close for GSM-7 text, but the failure does not: Bird never truncates, so an over-length body is rejected with a 422 instead of trimmed.
  • Nothing on the Messages API corresponds to category. Decide per message type whether it is transactional, marketing, authentication, or service. Authentication traffic in particular should be labelled as such rather than left in a marketing default.
  • Twilio's test credentials map onto simulated destinations. The magic numbers you already test with, including +15005550006 and +15005550001, produce synthesized outcomes here too, with two differences: there is no separate test credential, and the sends are billed. The outcomes are listed in the main guide.

Carry over opt-outs

Twilio scopes an opt-out to the sender or the Messaging Service that received the STOP, which is how a Bird suppression works too: one sender-and-subscriber pair, so your other senders keep reaching that subscriber. The pairs port directly.
Getting the list out is the hard part. Twilio's Advanced Opt-Out does not expose blocked numbers through the Console or the REST API, so ask Twilio support to export the opt-out list for each Messaging Service before you cut over. Failing that, reconstruct it from your own records: every opt-out began as an inbound message whose body matched a stop keyword, and those are in your Messages logs and in whatever your inbound webhook stored. Import the result through the suppression loop. Each suppression is one pair, so a subscriber who stopped three of your senders takes three calls. Reading and managing suppressions carries the command, and the reason a manual one blocks every category including transactional.
Once you are here, the handling itself is Bird's. Twilio answers STOP, UNSUBSCRIBE, END, QUIT, STOPALL, REVOKE, OPTOUT, and CANCEL automatically, with START and UNSTOP opting back in and HELP returning support text. Bird does the same job from its own keyword catalog per country, so 21610 becomes a Bird rejection carrying recipient_opted_out, and the custom keywords you configured on a Messaging Service become keyword rules.

Translate delivery statuses

OutcomeTwilio MessageStatusBird
API accepted the messagequeued, acceptedsms.accepted
Handed to the carriersending, sentsms.sent
Carrier confirmed deliverydeliveredsms.delivered
Carrier reported no receiptundeliveredsms.undelivered
Permanent failurefailedsms.failed
Refused before sendingrequest errorsms.rejected
Validity window elapsed(none)sms.expired
Scheduled or cancelledscheduled, canceledno equivalent yet
Three mechanics change with the names:
  • Endpoints replace callback URLs. Twilio posts to the StatusCallback on the message or the Messaging Service. Bird delivers to endpoints your workspace registers, each subscribed to the event types it wants, so a new consumer is a new subscription rather than a redeploy.
  • Signed JSON replaces form-encoded posts. Twilio sends application/x-www-form-urlencoded with an X-Twilio-Signature header; Bird sends JSON signed per Standard Webhooks. Swap the verification for the recipe in Webhooks & events.
  • Inbound messages arrive as events. Twilio's per-number "A message comes in" webhook expects a TwiML response your app can use to auto-reply. Bird emits sms.received to the same subscribed endpoint as everything else, and there is no response body that sends a reply: answer by calling the send endpoint, or let keyword rules answer for you.
Register the endpoint once, naming the event types your handler wants: the sms.* events in the table above are the list to subscribe to, and there is no wildcard that stands in for them. Create an endpoint has the command, why the catalog has to be enumerated, and the one thing to get right on the first call, which is storing the signing secret the response shows exactly once.
Twilio's numeric error codes have no one-to-one map. Bird reports a failure with a standardized error code such as invalid_destination, blocked_by_carrier, sender_unregistered, or recipient_opted_out; the full list is on the events page. Map your alerting to those rather than to the 30000-range codes.

Cut over

Destinations, senders, and the traffic ramp are provider-independent and covered in the main guide. Two Twilio-specific items belong on the cutover plan: your 10DLC brand and campaign are registered with The Campaign Registry through Twilio and do not transfer, so the same registration is submitted again through Bird; and numbers you own at Twilio need a port that support arranges, on its own schedule rather than yours.
Because that is a fresh registration rather than a transfer, start from Register for 10DLC: it covers what each field means, the entity types the registry recognizes, and the requirements call that tells you what to supply before you create the brand, which is the chargeable step.

Next steps