Migrate SMS from Infobip
This page maps Infobip's SMS API, Blocklist, and delivery reports to Bird. Follow the main migration guide in order and use these mappings for steps 3, 4, and 5.
Two differences do most of the work. Infobip's payload is built for the bulk case, so one message to one person is an array of messages, each holding an array of destinations, with the words two levels down in content.text; POST /v1/sms/messages takes from, to and text at the top level. And your Infobip base URL is personalised per account, in the shape xxxxx.api.infobip.com, authenticated with Authorization: App <key>. Bird sends from a regional host with a bearer key, so the host your code holds changes at the same time as the payload shape.
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.
Code example
I am moving an SMS integration from Infobip 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/infobip.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 Infobip usage in this repository before you change anything: every place that builds the nested messages/destinations/content payload, my personalised base URL wherever it is configured, the Authorization: App header, the delivery-report handler, and anywhere I read or write the Blocklist. Show me the list before you touch any of it.
4. Tell me early about two things. First, my base URL is per-account and Bird sends from a REGIONAL host, so tell me everywhere that host is set, including environment variables and deploy config, not just the code. Second, how many senders I am carrying over, because my Blocklist is one entry per subscriber for the whole account while a Bird suppression is one sender-and-subscriber pair, so the list multiplies by the number of senders and I want that number before anything is imported.
5. Then take only the paths that apply to me.
- Countries and senders: https://bird.com/docs/guides/sms/migrate.md covers enabling destinations and setting up a sender. Ask me which countries I actually send to rather than enabling everything, because an enabled destination I never use is exposure to SMS pumping rather than reach.
- Opt-outs: export the Blocklist over their API, expand it to pairs, and import following https://bird.com/docs/guides/sms/opt-outs-and-keywords.md. Show me the pair count before you start.
- Sending and events: flatten the send and repoint the delivery report, using the mapping tables on the provider page and https://bird.com/docs/guides/webhooks.md.
- US long codes: my 10DLC brand and campaign do not transfer and must be registered again, following https://bird.com/docs/guides/sms/10dlc.md. Submitting a registration is chargeable, so show me what you are about to submit and wait for me to say yes before you submit anything.
6. Test the ported path against Bird's simulated destinations before any real traffic, following https://bird.com/docs/guides/sms/migrate.md. They exercise the send and the webhook handler against real API responses and real signed deliveries without reaching a handset, but a simulated send is billed at the destination's normal rate, so tell me how many you plan to send and keep the smoke test to that. The United States must be enabled and `from` must be a sender valid for the US, or the test numbers reject.
7. Stop and ask me wherever a step needs a decision. Do not point production traffic at Bird, and do not retire the Infobip path, until I have seen the simulated-destination results and told you to go ahead. Finish by telling me what is left that only a person can do.Map the send call
The rename table is short because the shape change is the work:
| What it does | Infobip | Bird |
|---|---|---|
| Recipient | messages[].destinations[].to | to (one per request) |
| Sender | messages[].sender | from |
| Body | messages[].content.text | text |
| Intent | (none) | category, required on free text |
| Delivery reports | webhooks.delivery, per message | a workspace webhook subscribed to sms.* |
| Round-trip context | webhooks.callbackData | metadata, but see the size note below |
| Campaign grouping | options.campaignReferenceId | tags, for filtering only; see below |
| Flash | options.flash | no equivalent |
| Validity | options.validityPeriod | no equivalent: validity_period is rejected |
| Delivery window | options.deliveryTimeWindow | no equivalent |
| Safe retries | (none in their generated clients) | Idempotency-Key header |
Porting notes:
- Three levels become none. The nesting exists to carry many messages and many destinations in one request. Sending one message to one person, Bird takes the three fields at the top level, so the builder that assembles the arrays is deleted rather than translated.
- callbackData is bigger than metadata. Infobip accepts up to 4000 characters and returns it on the delivery report. Bird's metadata is capped at 2 KB serialized and is echoed on every event for the message, not just the terminal one. The echo is the better deal; the cap is not, so anything approaching the limit has to be trimmed to a key you can look up rather than carried whole.
- campaignReferenceId gets you the filtering and not the campaign. Bird's tags are {name, value} pairs that become query dimensions, so you can slice analytics by campaign the way you did. What does not come with them is a campaign object: nothing in Bird is created, named, or reportable as a campaign because a tag mentions one.
- campaignReferenceId is not an idempotency key. Infobip defines it as an ID for tracking a campaign's performance, so it groups but does not deduplicate. If you were relying on it to make a retry safe, you were not covered; the Idempotency-Key header is what does that here.
- Nothing corresponds to category. Infobip's message options cover validity, delivery window, flash and regional settings, and none of them declares why the message is being sent. Decide per message type whether it is transactional, marketing, authentication, or service.
- Two option fields have no home. validityPeriod is reserved and answers 422 SMSUnsupportedFeature; deliveryTimeWindow has no counterpart, so scheduling windows move into your own dispatcher.
Carry over opt-outs
Infobip keeps a Blocklist: a list of the recipients who have opted out of your communication, managed through the Blocklist API or through People in the web interface, with a send to anyone on it refused. Keyword triggers add to it automatically, so a subscriber texting STOP lands there without your application doing anything.
That makes the export the easiest of any provider in this set, and the expansion the largest. A Blocklist entry is one subscriber for the whole account; a Bird suppression is one sender-and-subscriber pair. So every entry becomes as many suppressions as you have senders: a thousand-entry Blocklist and six senders is six thousand records. Work out the multiplier before you start, because it is the difference between an import that takes a minute and one that needs batching and a progress log.
That expansion buys something, and it is worth knowing so nobody treats it as pure overhead. Because Bird stores pairs, a subscriber who stopped your marketing sender still hears from your authentication sender, which an account-wide blocklist cannot express. If your intent really is account-wide, the pairs reproduce it exactly; if it was never intended to be that broad, the migration is the moment you can narrow it.
Import through the suppression loop. Reading and managing suppressions carries the command, and the reason a manual suppression blocks every category including transactional.
Once you are here, Bird answers the stop keywords itself from its own catalog per country, so the keyword triggers you configured have no counterpart to rebuild, and any custom ones become keyword rules. Reasons stack rather than merge, so a pair you imported as manual that later texts STOP holds two records, and messages stay stopped until both have ended.
Translate delivery statuses
Infobip reports a status group and a status name on each delivery report, and Bird emits an event type:
| Outcome | Infobip status group | Bird |
|---|---|---|
| API accepted the message | PENDING | sms.accepted |
| Handed to the carrier | PENDING | sms.sent |
| Carrier confirmed delivery | DELIVERED | sms.delivered |
| Carrier reported no receipt | UNDELIVERABLE | sms.undelivered |
| Permanent failure | REJECTED | sms.failed |
| Refused before sending | REJECTED | sms.rejected |
| Validity window elapsed | EXPIRED | sms.expired |
EXPIRED is the row to read closely, because it covers two different things on their side and only one of them exists here. Infobip expires a message either when their own platform's validity period runs out, which defaults to 48 hours, or when the operator returns expired as the final status. Bird sets no validity window of its own and runs no timer that ends a message, so sms.expired only ever comes from the carrier's delivery receipt. The operator-reported half maps across; the platform-timer half has no counterpart, and a message that would have expired on their clock stays in flight here until the carrier decides.
REJECTED appears twice on purpose. Infobip uses it both for a message it refused itself and for one the operator returned as rejected, which are Bird's sms.rejected and sms.failed respectively. The status name inside the group is what tells them apart, so a handler that branched only on the group needs the name once it is here. PENDING also covers two rows, because it is the group the message sits in from acceptance until a terminal report arrives.
Three mechanics change with the names:
- Subscriptions replace per-message webhooks. Infobip names a webhook on each message, so the destination is chosen by whoever writes the call, and the content type is chosen with it. Bird delivers JSON to endpoints your workspace registers, each subscribed to the event types it wants, so a second consumer is a second subscription rather than a change at every call site.
- You lose the per-message choice, including XML. Infobip lets a message pick JSON or XML and attach up to 4000 characters of callback data. Bird sends JSON only, and callbackData becomes metadata, which is echoed on every event for that message rather than only on the report.
- Pull becomes push. Infobip lets you fetch reports from a reports endpoint as well as receive them. Bird has no equivalent poll for events; subscribe, and read message state through the API when you need it on demand.
Register the endpoint once, naming the event types your handler wants: the sms.* events above are the list to subscribe to, and there is no wildcard that stands in for them. Bird sends JSON signed per Standard Webhooks; Create an endpoint has the command and the one thing to get right on the first call, which is storing the signing secret the response shows exactly once.
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 Infobip's numeric group and name pairs.
Cut over
Destinations, senders, and the traffic ramp are provider-independent and covered in the main guide. Three Infobip-specific items belong on the cutover plan.
The host moves, and it is configuration rather than code. Your Infobip base URL is issued per account; Bird sends from a regional host chosen when your workspace was created. Find every place that host is set before the cutover, including environment variables, secrets managers and deploy manifests, because a missed one fails at runtime rather than at build.
Your 10DLC brand and campaign are registered with The Campaign Registry through Infobip's number registration API and do not transfer, so the same registration is submitted again through Bird. 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.
Numbers you own at Infobip 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 report handler moves to
- Webhooks & events: endpoint setup and Standard Webhooks verification