Documentation
Sign inGet started

Migrate SMS from another provider

Use this guide to move production SMS from another provider to Bird. Two things gate a first send here that your current provider handles differently, so they come before the code: the countries you send to, and the sender you send from. After those, port the send call, carry your opt-out list across, repoint delivery reports at webhooks, and test against simulated destinations before you move real traffic.
The migration checklist:
  1. Enable your destination countries
  2. Set up a sender
  3. Map the send call to POST /v1/sms/messages
  4. Carry over your opt-out list
  5. Switch delivery reports to webhooks
  6. Test against simulated destinations before cutover
Steps 3, 4, and 5 depend on which provider you're leaving. Your provider guide has the field-by-field payload mapping, the status and event translation, and where to get your opt-out list out.
Start with steps 1 and 2. Sender registration is the long pole in an SMS migration: carrier and registry review runs in days, while the code change runs in an afternoon.

1. Enable your destination countries

Every workspace has a default-deny destination allowlist that starts with only your organization's home country enabled. A send anywhere else returns 422 SMSDestinationNotEnabled before Bird resolves a sender, so an integration you ported faithfully still fails on its first international message until you open the country.
Enable every country you send to today under SMS > Destinations. Take the list from your current provider's message logs rather than from memory: a country you forget is a silent gap on cutover day, and a country you enable but never use is exposure you don't need. Deny by default is also what limits the damage from SMS pumping, where fraudulent traffic to premium ranges is billed to you.

2. Set up a sender

On a free-text send, from is the sender your recipient sees, and it takes one of three shapes: an alphanumeric sender ID, a phone number in E.164 that your workspace owns, or a short code. Which shapes work depends on the destination country, and a sender that isn't valid there is rejected with a 422 naming the reason. Sending SMS has the rules per shape.
How you get each one:
  • Alphanumeric sender IDs you create yourself under SMS > Senders. Where the destination country requires the sender ID to be registered, submit the registration there and wait for approval before you route traffic to it.
  • US A2P traffic needs either a registered 10DLC brand and campaign, set up under SMS > 10DLC, or a verified toll-free number. The US does not accept alphanumeric sender IDs at all, so a European sender ID that works everywhere else has no US equivalent.
  • Dedicated numbers are allocated to your workspace by Bird rather than created on the Senders page. Contact support with the countries and number types you need.
  • Keeping your current numbers is not self-serve: Bird has no port-in flow you can drive from the dashboard. If your subscribers reply to numbers you own today, raise the port with support before you schedule a cutover date, and plan for the port and the code change to be separate events.
A template send is the exception to all of this. It supplies the body, the category, and the sender, so from is not accepted alongside it and Bird picks a sender that is valid for the destination.

3. Map the send call

The single-send endpoint is POST /v1/sms/messages. Build a JSON payload with to, from, text, and category, and a successful call returns 202 Accepted with an sms_-prefixed message ID. Delivery happens after the response and reaches you through webhook events and the read endpoints. The full payload lives in Sending SMS; the field-by-field mapping from your current payload is in your provider guide.
Before you port code, account for these differences:
  • One recipient per request. Bird has no recipients array. If your current provider fans one call out to many numbers, that becomes one call per recipient, or one batch of independent messages in a single request.
  • category is required on free text, and it is transactional, marketing, authentication, or service. Most providers infer intent from the campaign or the sender; here you declare it per message, and per-country compliance rules such as opt-out policy and quiet hours key on it. Get it right on the port rather than defaulting everything to one value.
  • The body is capped in segments, and Bird does not truncate. A longer body is rejected with a 422. Non-GSM-7 characters more than halve what fits in a segment, so if your current provider silently transliterated curly quotes and dashes, set options.smart_encoding to keep the segment counts you're used to. It is off by default because it alters the body you composed.
  • Use tags for filter dimensions and metadata for context. Tags are {name, value} pairs you can filter and slice analytics by; metadata is arbitrary JSON that Bird stores, returns on reads, and echoes on every webhook event. A single client reference field at your old provider usually maps to metadata.
  • Scheduling and MMS have no equivalent here. scheduled_at, media_urls, validity_period, and per-recipient personalization are reserved fields, rejected with 422 SMSUnsupportedFeature. Those parts of your integration do not move with the rest: hold scheduled sends in your own queue and call the send endpoint at the moment of delivery.
  • Retries are safe with Idempotency-Key. Send a unique key per logical message and a retry after a timeout replays the original response instead of sending twice. See Idempotency.

4. Carry over your opt-out list

Import your opt-outs before the first production send. Messaging someone who told your old provider to stop is the compliance failure that ends a migration badly, and neither the carrier nor the regulator cares which vendor lost the record.
A Bird suppression covers a sender-and-subscriber pair, which is how most providers scope opt-outs too: the subscriber who stopped your marketing number is not thereby stopped on your support number. Add each pair with POST /v1/sms/suppressions:
Contoh kode
while IFS=, read -r destination originator; do
  curl -s -X POST https://us1.platform.bird.com/v1/sms/suppressions \
    -H "Authorization: Bearer $BIRD_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"destination\": \"$destination\", \"originator\": \"$originator\"}"
done < opt-outs.csv
The same import runs from the CLI as bird sms suppressions add --destination +15550001234 --originator +15557654321.
Two things to know about the import:
  • Both ends are required, so stopping all of your senders is one call per sender. The call is idempotent: 201 records a new suppression, 200 returns the manual one already in place, so re-running a partial import is safe.
  • Imported pairs get reason: manual, which blocks every category including transactional. That is stricter than a suppression Bird records itself from a stop keyword. If a subscriber opted out of marketing only, decide deliberately whether to import that pair.
From here you generally delete code rather than port it. Bird answers STOP, START, and HELP on your numbers in supported countries, records the suppression, and enforces it on every send, so the keyword handler you wrote at your old provider has no job. Custom campaign keywords and custom replies are configured under SMS > Keyword rules. See Opt-outs and keywords for coverage per country.

5. Switch delivery reports to webhooks

Register one endpoint with POST /v1/webhooks and subscribe it to an explicit list of event types. This is the structural change most providers require: instead of a callback URL per message or per number, your workspace has endpoints, and each endpoint subscribes to the events it wants.
Bird's event names follow resource.action. The happy path is sms.accepted, then sms.sent, then sms.delivered, with sms.undelivered, sms.failed, sms.expired, and sms.rejected covering the rest, and sms.received carrying replies to your numbers. The translation from your current provider's status vocabulary is in your provider guide, and the per-event payloads are in SMS events.
Correlation ports cleanly. Every event carries sms_id, workspace_id, to, and from, and echoes the tags and metadata from the send, so your handler reads your own identifiers straight off the event instead of looking the message up.
Two mechanics to port with the handler:
  • Deliveries are signed per Standard Webhooks, using the webhook-id, webhook-timestamp, and webhook-signature headers with an HMAC-SHA256 over {id}.{timestamp}.{raw body}. Providers that sign with their own scheme need the verification swapped out; the recipe is in Webhooks & events.
  • Delivery is at-least-once and unordered. Deduplicate on webhook-id and sort by the payload timestamp, never by arrival order.
Inbound messages follow the same model. Subscribe to sms.received once for the workspace rather than configuring an inbound URL per number, and remember that Bird still emits sms.received for a reply that matched a stop keyword, after recording the suppression.

6. Test against simulated destinations

Bird synthesizes delivery outcomes for a set of test destinations, so you can exercise your ported send path and your webhook handler against real API responses and real signed deliveries without a handset. These are the same numbers several providers use for test credentials, and a message to one of them never reaches a carrier.
DestinationWhat your integration sees
+15005550001Rejected at submission with invalid_destination
+15005550002sms.sent, then sms.undelivered with unreachable
+15005550003sms.sent, then sms.failed with provider_unavailable
+15005550004sms.sent, then sms.failed with blocked_by_carrier
+15005550006sms.sent, then sms.delivered
+15005550009sms.sent, then sms.failed with recipient_opted_out
Three conditions apply, and the first two catch people out on a fresh workspace:
  • These are US numbers, so the United States must be enabled under Destinations, and from must be a sender that is valid for the US. An alphanumeric sender ID is rejected there.
  • A simulated send is billed at the destination's normal rate. Nothing reaches a handset, but the wallet charge is real, so size your smoke test accordingly.
  • The outcome comes from the destination alone. There is no separate test credential, and no test mode to switch off.
A workable smoke test sends to +15005550006 and asserts your handler walks sms.accepted to sms.sent to sms.delivered; sends to +15005550002 and +15005550009 and asserts your failure and opt-out handling fire on the right error code; and sends one real message to a handset you control to confirm the sender and the body render the way you expect.
Then cut over by share of traffic rather than all at once. Move a small percentage of production sends to Bird, watch the SMS log and metrics for delivery rates and error codes against what your old provider reported for the same routes, and raise the share as the numbers hold. Keep the old integration deployable until the first full billing period looks right.

Migrating from a specific provider

  • Twilio: form-encoded PascalCase to JSON, Messaging Services to senders, StatusCallback to subscribed webhooks
  • Bird Connectivity Platform: the rest.messagebird.com API, originator and recipients to from and to, reportUrl GET callbacks to signed webhooks

Next steps

  • Sending SMS: the full send payload, senders, segments, and the async 202 model
  • Opt-outs and keywords: what Bird answers for you, and how to manage suppressions
  • SMS events: the event vocabulary and per-event payloads
  • Webhooks & events: endpoint setup, signature verification, retries and replay