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.
Map the send call
| What it does | Twilio | Bird |
|---|---|---|
| Recipient | To | to (one per request) |
| Sender | From or MessagingServiceSid | from |
| Body | Body | text |
| Content template | ContentSid + ContentVariables | template + template.parameters |
| Intent | (none) | category, required on free text |
| Filterable labels | (none) | tags: {name, value} pairs |
| Round-trip context | your own store, keyed by SID | metadata: arbitrary JSON, echoed on every event |
| Delivery reports | StatusCallback | a workspace webhook subscribed to sms.* |
| Transliteration | SmartEncoded | options.smart_encoding (default false) |
| Safe retries | (none on Messages) | Idempotency-Key header |
| Scheduling | ScheduleType + SendAt | no equivalent: scheduled_at is rejected |
| Media | MediaUrl | no equivalent: media_urls is rejected |
| Validity | ValidityPeriod | no equivalent: validity_period is rejected |
| Link shortening | ShortenUrls | no 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.
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
| Outcome | Twilio MessageStatus | Bird |
|---|---|---|
| API accepted the message | queued, accepted | sms.accepted |
| Handed to the carrier | sending, sent | sms.sent |
| Carrier confirmed delivery | delivered | sms.delivered |
| Carrier reported no receipt | undelivered | sms.undelivered |
| Permanent failure | failed | sms.failed |
| Refused before sending | request error | sms.rejected |
| Validity window elapsed | (none) | sms.expired |
| Scheduled or cancelled | scheduled, canceled | no 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.
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.
Next steps
- Sending SMS: the payload you are porting to, in full
- Opt-outs and keywords: keyword coverage per country and suppression management
- SMS events: the event vocabulary your status handler moves to
- Webhooks & events: endpoint setup and Standard Webhooks verification