OTT channels and fallback
A navigator can route over OTT channels — RCS and WhatsApp — before it falls back to SMS. This page covers enrolling them, what an attempt sequence looks like, and which webhooks tell you what happened.
All identifiers, agent IDs and phone numbers on this page are examples. Replace them with your own.
Enrol an OTT channel
Add OTT channels to a navigator with settings.ottChannels. Each entry names a channel and may be scoped to the countries where that sender is launched.
A settings update replaces the whole settings object. Send every field the navigator should keep — including useCaseType — or it is dropped.
PATCH
/workspaces/{workspaceId}/navigators/{navigatorId}
Update a navigator by ID
Treść żądania
name
string
wymagane
The name of the navigator.
type
string
wymagane
The type of navigator defines how the navigator selects a channel for a message. * messaging - navigator configured with a pool of channels and performs channel selection based on channel availability and best originator type for a recipient country. The best originator type for a recipient country is determined by the strategy - prioritized list of originator types for each country. At this moment, the default pre-configured strategy (set of country policies) is used see https://docs.bird.com/applications/channels/channels/supported-channels/sms/concepts/choosing-the-right-sender-availability-and-restrictions-by-country
Possible values: messaging, use-case-messaging, otp
settings
object
Pokaż parametry podrzędne
settings.channels
array of object
wymagane
List of channels to be used
Pokaż parametry podrzędne
settings.channels.channelId
string
wymagane
settings.channels.countries
array of string
ISO 3166-1 alpha-2 codes this channel may be used for. Omit or leave empty to allow every country. RCS availability is granted per carrier per country, so an RCS channel should always name the countries it covers.
settings.preferredChannels
array of object
Channels attempted in this order ahead of anything the country policy yields. Listing a channel here is the opt-in; a channel present only in channels stays policy-gated however its platform routes.
Pokaż parametry podrzędne
settings.preferredChannels.channelId
string
wymagane
settings.preferredChannels.countries
array of string
ISO 3166-1 alpha-2 codes this channel may be used for. Omit or leave empty to allow every country. RCS availability is granted per carrier per country, so an RCS channel should always name the countries it covers.
settings.useCaseType
string
wymagane
Possible values: otp, transactional, marketing, conversation
settings.ottChannels
array of object
OTT channels the navigator attempts before SMS, in order. Each entry may be scoped to the countries where its sender is launched.
Pokaż parametry podrzędne
settings.ottChannels.channelId
string
wymagane
settings.ottChannels.countries
array of string
ISO 3166-1 alpha-2 codes this channel may be used for. Omit or leave empty to allow every country. RCS availability is granted per carrier per country, so an RCS channel should always name the countries it covers.
settings.ottChannelsIds
object
Enrols one OTT channel per platform key. Channels enrolled this way join the navigator's pool without being preferred, so the country policy decides whether they are used. Use ottChannels to attempt them ahead of SMS, in a chosen order, and to scope them by country.
Pokaż parametry podrzędne
settings.ottChannelsIds.whatsapp
string
Channel for WA
settings.ottChannelsIds.rcs
string
Channel for RCS
Przykład kodu
curl -X PATCH 'https://api.bird.com/workspaces/{workspaceId}/navigators/{navigatorId}' \
-H 'Authorization: AccessKey YOUR_ACCESS_KEY'Przykład kodu
{
"name": "transactional",
"type": "use-case-messaging",
"settings": {
"useCaseType": "transactional",
"ottChannels": [
{
"channelId": "a1b2c3d4-1111-4aaa-8bbb-000000000001",
"countries": ["US", "GB"]
},
{
"channelId": "a1b2c3d4-2222-4aaa-8bbb-000000000002"
}
]
}
}- Order matters. ottChannels is attempted in the order you list it, ahead of SMS. Above, the first channel is tried first, then the second, then the navigator's SMS senders.
- countries is an allowlist of ISO 3166-1 alpha-2 codes. Omit it, or leave it empty, to allow every country.
- One channel per platform. A navigator uses at most one RCS channel and one WhatsApp channel.
RCS availability is granted per carrier per country, so scope an RCS channel to the countries where its agent is actually launched. An unscoped RCS channel is attempted worldwide and will fail wherever the agent is not live — spending an attempt before the fallback runs.
Confirm what the navigator will use
Coverage shows the senders per country, in attempt order, so you can check the scoping took effect. An RCS channel scoped to US and GB appears under those countries and nowhere else.
GET
/workspaces/{workspaceId}/navigators/{navigatorId}/coverage
Get the navigator coverage by ID configured for a workspace
Przykład kodu
curl -X GET 'https://api.bird.com/workspaces/{workspaceId}/navigators/{navigatorId}/coverage' \
-H 'Authorization: AccessKey YOUR_ACCESS_KEY'Sending
Send to a navigator exactly as before — the navigator decides the channels.
Przykład kodu
curl -X POST 'https://api.bird.com/workspaces/{workspaceId}/navigators/{navigatorId}/messages' \
-H 'Authorization: AccessKey {your-access-key}' \
-H 'Content-Type: application/json' \
-d '{
"receiver": {
"contacts": [
{ "identifierKey": "phonenumber", "identifierValue": "+15555550123" }
]
},
"body": { "type": "text", "text": { "text": "Your order has shipped." } }
}'Content per platform
Some platforms will not carry a plain text body. WhatsApp, for example, only sends when the message supplies a WhatsApp template. Use templates to give each platform its own content, keyed by platform ID:
Przykład kodu
{
"receiver": {
"contacts": [
{ "identifierKey": "phonenumber", "identifierValue": "+15555550123" }
]
},
"body": { "type": "text", "text": { "text": "Your order has shipped." } },
"templates": {
"whatsapp": {
"projectId": "b2c3d4e5-3333-4aaa-8bbb-000000000003",
"version": "c3d4e5f6-4444-4aaa-8bbb-000000000004",
"locale": "en"
}
}
}A platform named in templates sends that template. A platform not named there sends body. So the request above sends the template over WhatsApp and the text over RCS and SMS.
A platform that requires a template and is not given one is skipped — the navigator moves to its next channel instead.
What fallback looks like
The navigator attempts one channel at a time. When an attempt reaches sending_failed or delivery_failed, the navigator tries the next channel in its order. Each try is recorded in navigatorData.attempts.
A US send over RCS with SMS fallback, where the RCS leg fails:
Przykład kodu
{
"id": "d4e5f6a7-5555-4aaa-8bbb-000000000005",
"status": "delivered",
"receiver": {
"contacts": [
{ "identifierKey": "phonenumber", "identifierValue": "+15555550123" }
]
},
"navigatorData": {
"attempts": [
{
"channelId": "a1b2c3d4-1111-4aaa-8bbb-000000000001",
"platformId": "rcs-google",
"messageId": "e5f6a7b8-6666-4aaa-8bbb-000000000006",
"attempt": 1,
"status": "sending_failed",
"createdAt": "2026-02-10T09:12:04.104Z",
"failure": {
"code": 12003,
"description": "Unsupported destination",
"source": { "name": "rcs-google" }
}
},
{
"channelId": "a1b2c3d4-3333-4aaa-8bbb-000000000007",
"platformId": "sms-messagebird",
"messageId": "f6a7b8c9-7777-4aaa-8bbb-000000000008",
"attempt": 2,
"status": "delivered",
"createdAt": "2026-02-10T09:12:05.882Z"
}
]
}
}Reading it:
- attempts[] is the account of what happened — one entry per channel tried, in order, each with its own status and failure.
- The navigator's own status is the outcome of the whole chain. RCS failed, SMS delivered, so the navigator message is delivered. A per-attempt failure is not a failed send.
- messageId on each attempt is the channel message for that leg. Use it against the channel endpoint to inspect that attempt on its own.
An attempt rejected before it reaches a platform never produces a channel message, and its messageId is all zeros.
When fallback does not happen
A rejection that applies to the recipient or the message as a whole is not something the next channel gets around, so the chain stops rather than trying it:
- The recipient has opted out (15016, Recipient opted out). Consent is recorded per platform, but a suppressed recipient is not reached over a different one.
- A hold-out — global, campaign or journey.
Everything else falls back normally. In these cases the navigator settles on sending_failed with channels still unattempted, and that verdict is final.
Suppressions are per channel
A suppression is recorded against a platform and an identifier — not against the contact alone. So a recipient who opted out of your SMS traffic is suppressed for sms, and nothing about that opt-out is known to rcs or whatsapp.
Enrol a new OTT channel and it starts with no suppressions of its own. A recipient who opted out of SMS is attempted over RCS, and because RCS is attempted first, that is the channel they are reached on. Copy your existing suppressions to the new platform before you send.
This also changes what fallback does for an opted-out recipient:
- Suppressed on the channel being attempted — the chain stops there. The recipient is not reached over the next channel either, and the attempt fails with 15016.
- Suppressed on SMS only, with RCS enrolled and not suppressed — the RCS attempt goes ahead and delivers. The SMS opt-out is never consulted, because it belongs to a different platform.
Copy your SMS suppressions to RCS
Suppressions are imported by CSV from Manage > Audience > Suppressions, using Import CSV. The importer accepts a platform type per row, so the same identifiers can be suppressed for an additional platform.
-
Export who you already suppress for SMS. Export is done from the contacts side, not the Suppressions page: go to Contacts > Advanced filters, add the condition If someone can or cannot receive marketing/transactional, choose cannot receive, choose SMS, then export from the page — or save it as a segment and export that. The result is the set of recipients SMS will not deliver to.
-
Re-target the rows at the new platform. Reshape the exported identifiers into the importer's columns, keeping the same scope and setting the platform type to the platform you are enrolling:Przykład kodu
emailaddress,phonenumber,marketing,transactional,platformtype ,+15555550123,true,false,rcsValid platformtype values are sms, email, whatsapp and rcs. Keep marketing and transactional the same as the SMS row so the new platform inherits the same scope — a recipient who opted out of marketing only should not become suppressed for transactional too. -
Import the file, then confirm the rows appear against the new platform before sending over it.
Do this for each OTT platform you enrol, and repeat it when you enrol a platform later — an import is a point-in-time copy, not an ongoing sync. Opt-outs that arrive afterwards are recorded only for the channel that received them.
Walkthroughs for each half: Manage suppression list for the export, Create a suppression list for the import, and How suppression works for the rules that apply once a recipient is suppressed.
Monitoring
There are two scopes, and they answer different questions.
| Scope | Event | Answers |
|---|---|---|
| Navigator | navigator.outbound | Did the message reach the recipient, over which channel, and what did each attempt do? |
| Channel | rcs-google.outbound, sms.outbound, whatsapp.outbound | What happened on one specific leg? |
Subscribe to navigator.outbound for delivery monitoring. It is the only event that tells you the outcome of the chain. Use the channel events when you need detail on an individual leg.
navigator.outbound fires as the navigator message is created and on each update, so you receive the chain progressing. Treat the status on each delivery as the current state, not a final one, until it is terminal — delivered, delivery_failed or sending_failed.
Subscribing
Create a subscription with service: channels and the event you want. navigator.outbound can be filtered by navigatorId; the channel events by channelId and messageStatus.
Przykład kodu
{
"service": "channels",
"event": "navigator.outbound",
"url": "https://example.com/webhooks/navigator",
"eventFilters": [
{ "key": "navigatorId", "value": "a7b8c9d0-8888-4aaa-8bbb-000000000009" }
]
}POST
/workspaces/{workspaceId}/webhook-subscriptions
To start receiving notifications via webhooks, the first step is to create a subscription. A webhook subscription specifies the destination URL for events and defines how they should be filtered. During setup, you can select which events to send to the specified URL.
You can create multiple webhook subscriptions to route different types of events to various URLs as needed.
Event filters are applied using AND operators, meaning that all specified criteria must be met for an event to be sent.
If you want to handle multiple interactionTypes, you’ll need to create separate webhook subscriptions for each.
Treść żądania
service
string
wymagane
The service that the webhook is subscribed to. For example, to get events regarding channels, the service would be channels.
Possible values: channels, numbers, payments, conversations, templates
event
string
wymagane
The event name identifies the webhook event, such as sms.outbound for notifications about SMS messages being sent.
eventFilters
array of object
Filters to apply to the events that are sent to the webhook. This is a key-value list of filters that are specific to the service that the webhook is subscribed to.
One example would be a key of channelId and a value of a UUID (in string format) that represents a channel.
Pokaż parametry podrzędne
eventFilters.key
string
wymagane
Possible values: channelId, platformId, channelStatus, interactionType, messageStatus, status, eventDestination, navigatorId
eventFilters.value
string
wymagane
template
string
url
string
wymagane
The URL of the webhook is used to send events to the webhook. The URL must be a valid URL that respects the established pattern and is accessible from the internet.
signingKey
string
The signing key for the webhook and can be used to verify the authenticity of the webhook.
oauth2
object
OAuth 2.0 client credentials configuration.
Pokaż parametry podrzędne
oauth2.tokenUrl
string
wymagane
The OAuth 2.0 token endpoint where access tokens are requested.
oauth2.clientId
string
wymagane
The OAuth 2.0 client identifier.
oauth2.clientSecret
string
wymagane
The OAuth 2.0 client secret.
oauth2.scopes
array of string
OAuth 2.0 scopes to request.
Treść odpowiedzi
id
string
wymagane
The unique identifier for the webhook subscription. This identifier is used to reference the webhook subscription in other API calls.
organizationId
string
wymagane
workspaceId
string
wymagane
service
string
wymagane
The service that the webhook is subscribed to. For example, to get events regarding channels, the service would be channels.
Possible values: channels, numbers, payments, conversations, templates
event
string
wymagane
The event name identifies the webhook event, such as sms.outbound for notifications about SMS messages being sent.
eventFilters
array of object
Pokaż atrybuty podrzędne
eventFilters.key
string
wymagane
Possible values: channelId, platformId, channelStatus, interactionType, messageStatus, status, eventDestination, navigatorId
eventFilters.value
string
wymagane
template
string
url
string
wymagane
The URL of the webhook is used to send events to the webhook. The URL must be a valid URL that respects the established pattern and is accessible from the internet.
signingKey
string
The signing key for the webhook and can be used to verify the authenticity of the webhook.
oauth2
object
OAuth 2.0 client credentials configuration.
Pokaż atrybuty podrzędne
oauth2.tokenUrl
string
wymagane
The OAuth 2.0 token endpoint where access tokens are requested.
oauth2.clientId
string
wymagane
The OAuth 2.0 client identifier.
oauth2.scopes
array of string
OAuth 2.0 scopes to request.
status
string
wymagane
Possible values: active, inactive
statusReason
string
A human-readable explanation for the current status of the webhook subscription.
createdAt
string
updatedAt
string
Przykład kodu
curl -X POST 'https://api.bird.com/workspaces/{workspaceId}/webhook-subscriptions' \
-H 'Authorization: AccessKey YOUR_ACCESS_KEY'Worked example: US RCS with SMS fallback
A navigator with an RCS channel scoped to US and an SMS sender behind it, sending to a US recipient whose carrier the RCS agent does not cover.
1. Send
Przykład kodu
curl -X POST 'https://api.bird.com/workspaces/{workspaceId}/navigators/a7b8c9d0-8888-4aaa-8bbb-000000000009/messages' \
-H 'Authorization: AccessKey {your-access-key}' \
-H 'Content-Type: application/json' \
-d '{
"receiver": {
"contacts": [
{ "identifierKey": "phonenumber", "identifierValue": "+15555550123" }
]
},
"reference": "order-4821",
"body": { "type": "text", "text": { "text": "Your order has shipped." } }
}'2. Response
202 Accepted. The navigator has picked its channels and taken the first attempt — it has not delivered anything yet.
Przykład kodu
{
"id": "d4e5f6a7-5555-4aaa-8bbb-000000000005",
"channelId": "a7b8c9d0-8888-4aaa-8bbb-000000000009",
"reference": "order-4821",
"status": "accepted",
"body": { "type": "text", "text": { "text": "Your order has shipped." } },
"meta": {
"navigatorId": "a7b8c9d0-8888-4aaa-8bbb-000000000009",
"navigatorMessageId": "d4e5f6a7-5555-4aaa-8bbb-000000000005"
},
"navigatorData": {
"attempts": [
{
"channelId": "a1b2c3d4-1111-4aaa-8bbb-000000000001",
"platformId": "rcs-google",
"messageId": "e5f6a7b8-6666-4aaa-8bbb-000000000006",
"attempt": 1,
"status": "accepted",
"createdAt": "2026-02-10T09:12:04.104Z"
}
]
}
}- id is the navigator message. Use it to poll, and expect it on every webhook for this send.
- Keep your own reference — it comes back on the navigator message and on each leg, which is the easiest way to tie webhooks to your records.
- Everything after this point is asynchronous. Track it rather than inferring the outcome from this response.
3. Track with navigator.outbound
You receive a delivery each time the navigator message changes, so you see the chain progress. For this send:
| # | payload.status | attempts[] in that payload |
|---|---|---|
| 1 | processing | 1: rcs-google accepted |
| 2 | processing | 1: rcs-google sending_failed — 12003 |
| 3 | processing | 1: rcs-google failed · 2: sms-messagebird accepted |
| 4 | delivered | 1: rcs-google failed · 2: sms-messagebird delivered |
The shape to notice: payload.status stays processing while the navigator still has channels to try, even though attempt 1 has already failed. It only settles once a leg delivers or the chain runs out. So a failed attempt is not a failed message, and you should not alert on one.
Delivery 2 — RCS has failed, the chain continues:
Przykład kodu
{
"service": "channels",
"event": "navigator.outbound",
"payload": {
"id": "d4e5f6a7-5555-4aaa-8bbb-000000000005",
"navigatorId": "a7b8c9d0-8888-4aaa-8bbb-000000000009",
"reference": "order-4821",
"status": "processing",
"attempts": [
{
"channelId": "a1b2c3d4-1111-4aaa-8bbb-000000000001",
"platformId": "rcs-google",
"messageId": "e5f6a7b8-6666-4aaa-8bbb-000000000006",
"attempt": 1,
"status": "sending_failed",
"createdAt": "2026-02-10T09:12:04.104Z",
"failure": {
"code": 12003,
"description": "Unsupported destination",
"source": { "name": "rcs-google" }
}
}
],
"createdAt": "2026-02-10T09:12:04.104Z",
"updatedAt": "2026-02-10T09:12:05.417Z"
}
}Delivery 4 — the final one. SMS delivered, so the message is delivered:
Przykład kodu
{
"service": "channels",
"event": "navigator.outbound",
"payload": {
"id": "d4e5f6a7-5555-4aaa-8bbb-000000000005",
"navigatorId": "a7b8c9d0-8888-4aaa-8bbb-000000000009",
"reference": "order-4821",
"status": "delivered",
"attempts": [
{
"channelId": "a1b2c3d4-1111-4aaa-8bbb-000000000001",
"platformId": "rcs-google",
"messageId": "e5f6a7b8-6666-4aaa-8bbb-000000000006",
"attempt": 1,
"status": "sending_failed",
"createdAt": "2026-02-10T09:12:04.104Z",
"failure": {
"code": 12003,
"description": "Unsupported destination",
"source": { "name": "rcs-google" }
}
},
{
"channelId": "a1b2c3d4-3333-4aaa-8bbb-000000000007",
"platformId": "sms-messagebird",
"messageId": "f6a7b8c9-7777-4aaa-8bbb-000000000008",
"attempt": 2,
"status": "delivered",
"createdAt": "2026-02-10T09:12:05.882Z"
}
],
"createdAt": "2026-02-10T09:12:04.104Z",
"updatedAt": "2026-02-10T09:12:06.203Z"
}
}Act on a terminal payload.status — delivered, delivery_failed or sending_failed. Each payload carries the whole attempts array, so the final one alone tells you the message arrived over SMS after RCS could not reach the destination. You do not need to accumulate the earlier deliveries.
Deliveries can arrive out of order, and the exact count varies with how each leg fails — a leg rejected before it reaches the platform produces fewer updates than one that fails after being sent. Treat updatedAt as the ordering, and the attempts array in the payload you are holding as the current picture.
4. Or track each leg with channel events
Subscribing to the channel events instead gives you one stream per platform. For the same send: an rcs-google.outbound for the failed RCS leg, then an sms.outbound for the SMS that delivered.
Each carries the navigator linkage in meta, so you can group the legs back onto one navigator message:
Przykład kodu
{
"service": "channels",
"event": "rcs-google.outbound",
"payload": {
"id": "e5f6a7b8-6666-4aaa-8bbb-000000000006",
"channelId": "a1b2c3d4-1111-4aaa-8bbb-000000000001",
"reference": "order-4821",
"status": "sending_failed",
"reason": "no carrier matches the destination",
"failure": {
"code": 12003,
"description": "Unsupported destination",
"source": { "name": "rcs-google" }
},
"meta": {
"navigatorId": "a7b8c9d0-8888-4aaa-8bbb-000000000009",
"navigatorMessageId": "d4e5f6a7-5555-4aaa-8bbb-000000000005"
},
"lastStatusAt": "2026-02-10T09:12:05.417Z"
}
}Note the trade-off: a channel event tells you what one leg did, but nothing about the message as a whole. The failed rcs-google.outbound above looks like a failure on its own — only the navigator knows SMS then delivered. If you rely on channel events alone, join on meta.navigatorMessageId and treat a leg failure as final only once no further legs arrive.
sms.outbound carries the full message object, for backward compatibility. Every other <platform>.outbound carries a status-focused payload — id, channelId, sender, receiver, reference, status, reason, failure, meta and timestamps — which is what you need to follow a leg, but not the message content.
Which to use
- Delivery reporting — navigator.outbound alone. It answers "did this reach the recipient", which is the question a fallback chain exists to change the answer to.
- Per-channel diagnostics — add the channel events. Useful for questions like "how often does RCS fail for US destinations", where you want the legs and their reasons separately.
Statuses
An attempt moves through these, and the navigator message reports the outcome of the chain:
| Status | Meaning |
|---|---|
| accepted | Taken for delivery. |
| processing | The navigator is working through its channels. |
| sent | Handed to the platform. |
| sending_failed | Could not be handed over. Triggers fallback. |
| delivered | Reached the recipient. Terminal. |
| delivery_failed | The platform could not deliver it. Triggers fallback. |
Failure codes worth recognising
| Code | Description | In a fallback chain |
|---|---|---|
| 12003 | Unsupported destination | The platform cannot reach this destination — an RCS agent not launched for the recipient's carrier or country. Falls back. |
| 12006 | Invalid message | The content is not valid for that platform, e.g. a platform needing a template. Falls back. |
| 15016 | Recipient opted out | The recipient is suppressed. Ends the chain — no fallback. |
| 14007 | Recipient in global holdout | Ends the chain — no fallback. |
failure.source.name names the leg that failed — rcs-google, whatsapp, sms-messagebird — or pre-processing when the message was rejected before it reached a platform.
Incoming messages
RCS and WhatsApp are two-way. If you route over them, subscribe to rcs-google.inbound and whatsapp.inbound so you do not miss replies.
Incoming messages are not linked to a navigator and carry no navigator information in meta.