Documentation
Sign inGet started

Migrate SMS from Telnyx

This page maps Telnyx's Messages API, messaging profiles, and delivery webhooks to Bird. Follow the main migration guide in order and use these mappings for steps 3, 4, and 5.
The send call is the closest to Bird's of any provider here: JSON, a bearer key, and the same field names. POST https://api.telnyx.com/v2/messages takes from, to and text, and so does POST /v1/sms/messages. What does not carry over is the messaging profile. Telnyx makes it the unit of almost everything: sender pool, webhook URL, opt-out scope, and the keyword configuration. Bird splits those across senders, webhook subscriptions and suppressions. Most of the work in this migration is unpicking that object.

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 Telnyx 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/telnyx.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 Telnyx usage in this repository before you change anything: the /v2/messages call sites and any SDK wrappers around them, every messaging_profile_id they name, the webhook handler that reads the delivery payload, and anywhere I verify a Telnyx signature. Show me the list before you touch any of it.
4. Tell me early how many messaging profiles I have and what differs between them, because Bird has no profile object and this is the step that decides how much work the rest is. Each profile's sender pool becomes senders chosen per send, its webhook URL becomes a workspace subscription, and its opt-out scope becomes suppression pairs. Also tell me if I use an alphanumeric sender ID anywhere: it has to be created again and registered again per country, which is not a transfer.
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: my opt-out state is scoped to a whole messaging profile, and a Bird suppression is one sender-and-subscriber pair. Work out how many pairs my list becomes and tell me the number before you import anything, then follow https://bird.com/docs/guides/sms/opt-outs-and-keywords.md.
   - Sending and events: port the send call and repoint the webhook, 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 Telnyx 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 doesTelnyxBird
Recipienttoto (one per request)
Senderfrom or messaging_profile_idfrom
Bodytexttext
Intent(none)category, required on free text
Delivery reportsthe profile's webhook URLa workspace webhook subscribed to sms.*
Round-trip contextyour own store, keyed by IDmetadata: arbitrary JSON, echoed on every event
Filterable labels(none)tags: {name, value} pairs
Safe retries(none documented)Idempotency-Key header
Mediamedia_urlsno equivalent: media_urls is rejected
Porting notes:
  • A messaging profile ID becomes a plain sender value. Telnyx resolves the number pool and its sending rules behind the profile. 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.
  • 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.
  • A retry is only safe on the Bird side. Telnyx's send reference documents no idempotency key, so a timeout there leaves you guessing. Send the Idempotency-Key header from the first port.

Carry over opt-outs

This is the step that surprises people, and the number to work out first is how many suppressions your list becomes.
Telnyx scopes an opt-out to the whole messaging profile: a subscriber who texts STOP to any one number on a profile is blocked from every number on that profile, and a send to them answers error 40300, "Blocked due to STOP message". Keeping separate opt-out lists for separate programs is done by keeping separate profiles.
Bird scopes a suppression to a sender-and-subscriber pair instead. So one Telnyx opt-out against a profile holding twelve numbers becomes twelve Bird suppressions, and a profile with a hundred numbers becomes a hundred. Count before you import: the multiplier is the number of senders you are carrying over from that profile, and it decides whether the import is a loop of hundreds or of tens of thousands.
The pair model is also why Bird needs no profile object to do what you were using extra profiles for. Separating opt-out state per program was a reason to split profiles on Telnyx; here it falls out of the sender, so a subscriber who stopped your marketing sender still hears from your authentication sender without any structure to maintain.
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, the handling itself is Bird's. The custom keywords and auto-responses you configured per profile through autoresp_configs become keyword rules, and a blocked send becomes a Bird rejection carrying recipient_opted_out rather than 40300.
Reasons stack rather than merge, which matters once traffic is flowing: 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. Resuming a subscriber you once imported means removing both.

Translate delivery statuses

OutcomeTelnyxBird
API accepted the messagequeuedsms.accepted
Handed to the carriersent, on message.sentsms.sent
Carrier confirmed deliverydelivered, on message.finalizedsms.delivered
Delivery faileddelivery_failedsms.undelivered
Permanent failuresending_failedsms.failed
Refused before sendingrequest errorsms.rejected
Validity window elapsed(none)sms.expired
The event shape changes, not just the words. Telnyx sends one message.finalized webhook carrying the terminal state in a status field, so your handler branches on a value inside a single event type. Bird emits distinct event types instead, and you subscribe to the ones you want, so the branch moves out of your code and into the subscription. That is why the left column above names an event and a status together and the right column names only an event.
Two more mechanics change with the names:
  • Subscriptions replace the profile's webhook URL. Telnyx posts delivery updates to the URL on the messaging profile, so the destination is a property of the profile every message was sent through. Bird delivers 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 to a shared object.
  • Standard Webhooks replaces Telnyx's signature scheme. Bird sends JSON signed per Standard Webhooks; swap the verification 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.
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 Telnyx-specific items belong on the cutover plan: your 10DLC brand and campaign are registered with The Campaign Registry through Telnyx and do not transfer, so the same registration is submitted again through Bird; and numbers you own at Telnyx need a port that support arranges, on its own schedule rather than yours.
Because that is a fresh registration rather than a transfer, 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.

Next steps