# Migrate SMS from Twilio

This page maps Twilio's Programmable Messaging API, Messaging Services, and status callbacks to Bird. Follow the [main migration guide](/docs/guides/sms/migrate) 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`](/docs/api/reference/create-sms-message) 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](/docs/guides/sms/sending-sms#reserved-fields) 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](/docs/guides/sms/templates), which selects a valid sender for the destination and rejects `from`.
- **A character limit becomes a [segment cap](/docs/guides/sms/sending-sms#segments-and-encoding).** 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](/docs/guides/sms/migrate#6-test-against-simulated-destinations).

## 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](/docs/guides/sms/migrate#4-carry-over-your-opt-out-list).

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](/docs/guides/sms/opt-outs-and-keywords#campaign-keywords).

## 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](https://www.standardwebhooks.com). Swap the verification for the recipe in [Webhooks & events](/docs/guides/webhooks#verify-signatures).
- **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](/docs/guides/sms/opt-outs-and-keywords) 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](/docs/guides/sms/events#failure-events). Map your alerting to those rather than to the 30000-range codes.

## Cut over

[Destinations](/docs/guides/sms/migrate#1-enable-your-destination-countries), [senders](/docs/guides/sms/migrate#2-set-up-a-sender), and the [traffic ramp](/docs/guides/sms/migrate#6-test-against-simulated-destinations) 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](/docs/guides/sms/sending-sms): the payload you are porting to, in full
- [Opt-outs and keywords](/docs/guides/sms/opt-outs-and-keywords): keyword coverage per country and suppression management
- [SMS events](/docs/guides/sms/events): the event vocabulary your status handler moves to
- [Webhooks & events](/docs/guides/webhooks): endpoint setup and Standard Webhooks verification