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.

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.
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.
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