Migrate SMS from Plivo
This page maps Plivo's Message API, Powerpacks, and delivery callbacks to Bird. Follow the main migration guide in order and use these mappings for steps 3, 4, and 5.
The send is the easy part. Both take JSON with lowercase field names, and both keep 10DLC registration beside the send rather than on a separate host. Two things change. Plivo's POST https://api.plivo.com/v1/Account/{auth_id}/Message/ authenticates with an Auth ID and Auth Token over HTTP Basic; POST /v1/sms/messages takes a bearer API key against your regional host, with no account segment in the path. And a Plivo Powerpack bundles a number pool, sticky sender behaviour and opt-out state into one object; Bird splits those across senders, suppressions and keyword rules, so there is nothing to recreate as a Powerpack.
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 Plivo 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/plivo.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 Plivo usage in this repository before you change anything: the Message API call sites and any SDK wrappers around them, every Powerpack UUID they name, the callback handler that reads message_state, and anywhere I verify X-Plivo-Signature-V2. Show me the list before you touch any of it.
4. Tell me early which of these I rely on, because none of them ports as a setting. Powerpacks have no counterpart, so a number pool becomes a sender chosen per send or a template send. Any alphanumeric sender ID has to be created again and registered again per country, which is not a transfer. And numbers I own at Plivo move by a port that Plivo and Bird support arrange between them, on their schedule rather than mine.
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 my DND list from the Plivo console and import it following https://bird.com/docs/guides/sms/opt-outs-and-keywords.md. A Bird suppression is one sender-and-subscriber pair, so tell me how many pairs my list becomes before you start, and read the Carry over opt-outs section of that page for why a US campaign opt-out expands into several.
- Sending and events: port the send call and repoint the callback, 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. Bird has no profile step. 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 Plivo 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
| What it does | Plivo | Bird |
|---|---|---|
| Recipient | dst | to (one per request) |
| Sender | src or powerpack_uuid | from |
| Body | text | text |
| Channel selector | type: sms, mms, whatsapp | the endpoint itself; /v1/sms/messages is SMS |
| Intent | (none) | category, required on free text |
| Delivery reports | url + method, per message | a workspace webhook; JSON POST only, see below |
| Round-trip context | your own store, keyed by UUID | metadata: arbitrary JSON, echoed on every event |
| Filterable labels | (none) | tags: {name, value} pairs |
| Safe retries | (none documented) | Idempotency-Key header |
| Media | media_urls | no equivalent: media_urls is rejected |
Porting notes:
- A Powerpack UUID becomes a plain sender value. Plivo resolves the number pool, sticky sender and local presence behind the UUID. Bird takes the sender itself in from, so choose it per send, or use a template send, which selects a valid sender for the destination and rejects from.
- type has no counterpart because the endpoint carries it. Plivo selects the channel per request; Bird's SMS, WhatsApp and other channels are separate endpoints. A codebase that switches type at runtime splits into calls to different endpoints.
- Nothing on the Message 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.
- A retry is only safe on the Bird side. Plivo's send reference documents no idempotency key or deduplication mechanism, so a timeout there leaves you guessing. Send the Idempotency-Key header from the first port so a retry cannot double-send.
Carry over opt-outs
Plivo's DND service blocks outbound messages from one Plivo number to one destination once that destination replies with an opt-out keyword. A blocked send comes back flagged with Plivo error code 200, which is one of their message error codes and not an HTTP status, however much it looks like one. That pairing is how a Bird suppression works too: one sender and one subscriber, so your other senders keep reaching that subscriber.
One thing expands, and it is the reason to count before you import. Inside a US 10DLC campaign, Plivo treats an opt-out from any one number as an opt-out from every number linked to that campaign. Bird stores pairs, so a subscriber who opted out of a four-number campaign becomes four suppressions rather than one. Work out how many pairs your list becomes before you start, because it decides whether the import is a loop of tens or of thousands.
Getting the list out is a console export rather than an API call: filter the numbers in the Plivo console, select them, and use Export CSV from the Choose Action menu. Import the result 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, the handling itself is Bird's. Bird answers the stop keywords from its own catalog per country, so a blocked send becomes a Bird rejection carrying recipient_opted_out rather than Plivo error code 200, and any custom keywords become keyword rules.
That matters again later, once traffic is flowing. Reasons stack rather than merge: a pair you imported as manual that then texts STOP gets a second record with reason keyword_stop, and messages stay stopped until every record for that pair has ended. So resuming a subscriber you once imported means removing both, and a resume that clears only the keyword record looks successful and changes nothing.
Translate delivery statuses
| Outcome | Plivo message_state | Bird |
|---|---|---|
| API accepted the message | queued | sms.accepted |
| Handed to the carrier | sent | sms.sent |
| Carrier confirmed delivery | delivered | sms.delivered |
| Carrier reported no receipt | undelivered | sms.undelivered |
| Permanent failure | failed | sms.failed |
| Refused before sending | rejected | sms.rejected |
| Validity window elapsed | (none) | sms.expired |
Two mechanics change with the names:
- Endpoints replace per-message callback URLs. Plivo takes a url on each send, so the destination is chosen by whoever writes the call. 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 change at every call site.
- Signed JSON posts replace a GET callback, if that is what you chose. Plivo's method selects GET or POST for the delivery report; Bird POSTs a JSON event and offers no GET. If you set method=GET, your handler reads the outcome out of query-string parameters, and that handler is a rewrite rather than a re-registration. The same is true one guide over, on the Connectivity Platform path.
- One signature scheme replaces three headers. Plivo signs callbacks with X-Plivo-Signature-V2, X-Plivo-Signature-Ma-V2 and X-Plivo-Signature-V2-Nonce. Bird sends JSON signed per Standard Webhooks, so the verifier is replaced rather than adjusted: swap it for the recipe in Webhooks & events.
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. 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.
Plivo's numeric error_code values 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.
Cut over
Destinations, senders, and the traffic ramp are provider-independent and covered in the main guide. Two Plivo-specific items belong on the cutover plan.
Your 10DLC brand and campaign are registered with The Campaign Registry through Plivo and do not transfer, so the same registration is submitted again through Bird. The chain is shorter here. Plivo registers a profile first and then a brand against it, under /v1/Account/{auth_id}/10dlc/; Bird has no profile object, so the business details Plivo holds on the profile are supplied on the brand itself. 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 Plivo need a port that support arranges, on its own schedule rather than yours. Start it early and it runs alongside the code change.
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 callback handler moves to
- Webhooks & events: endpoint setup and Standard Webhooks verification