Sign inGet started

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
Payload permintaan
name
string
wajib
The name of the navigator.
type
string
wajib
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
Tampilkan parameter turunan
settings.channels
array of object
wajib
List of channels to be used
Tampilkan parameter turunan
settings.channels.channelId
string
wajib
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.
Tampilkan parameter turunan
settings.preferredChannels.channelId
string
wajib
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.
Contoh kode
curl -X PATCH 'https://api.bird.com/workspaces/{workspaceId}/navigators/{navigatorId}' \
  -H 'Authorization: AccessKey YOUR_ACCESS_KEY'
Contoh kode
{
  "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
Contoh kode
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.
Contoh kode
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:
Contoh kode
{
  "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:
Contoh kode
{
  "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.
  1. 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.
  2. 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:
    Contoh kode
    emailaddress,phonenumber,marketing,transactional,platformtype
    ,+15555550123,true,false,rcs
    
    Valid 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.
  3. 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.
ScopeEventAnswers
Navigatornavigator.outboundDid the message reach the recipient, over which channel, and what did each attempt do?
Channelrcs-google.outbound, sms.outbound, whatsapp.outboundWhat 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.
Contoh kode
{
  "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.
Payload permintaan
service
string
wajib
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
wajib
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.
Tampilkan parameter turunan
eventFilters.key
string
wajib
Possible values: channelId, platformId, channelStatus, interactionType, messageStatus, status, eventDestination, navigatorId
eventFilters.value
string
wajib
template
string
url
string
wajib
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.
Tampilkan parameter turunan
oauth2.tokenUrl
string
wajib
The OAuth 2.0 token endpoint where access tokens are requested.
oauth2.clientId
string
wajib
The OAuth 2.0 client identifier.
oauth2.clientSecret
string
wajib
The OAuth 2.0 client secret.
oauth2.scopes
array of string
OAuth 2.0 scopes to request.
Payload respons
id
string
wajib
The unique identifier for the webhook subscription. This identifier is used to reference the webhook subscription in other API calls.
organizationId
string
wajib
workspaceId
string
wajib
service
string
wajib
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
wajib
The event name identifies the webhook event, such as sms.outbound for notifications about SMS messages being sent.
eventFilters
array of object
Tampilkan atribut turunan
eventFilters.key
string
wajib
Possible values: channelId, platformId, channelStatus, interactionType, messageStatus, status, eventDestination, navigatorId
eventFilters.value
string
wajib
template
string
Used for our Exit APIs for Twilio and Sinch. More information can be found here for Twilio and here for Sinch.
url
string
wajib
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.
Tampilkan atribut turunan
oauth2.tokenUrl
string
wajib
The OAuth 2.0 token endpoint where access tokens are requested.
oauth2.clientId
string
wajib
The OAuth 2.0 client identifier.
oauth2.scopes
array of string
OAuth 2.0 scopes to request.
status
string
wajib
Possible values: active, inactive
statusReason
string
A human-readable explanation for the current status of the webhook subscription.
createdAt
string
updatedAt
string
Contoh kode
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

Contoh kode
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.
Contoh kode
{
  "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.statusattempts[] in that payload
1processing1: rcs-google accepted
2processing1: rcs-google sending_failed12003
3processing1: rcs-google failed · 2: sms-messagebird accepted
4delivered1: 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:
Contoh kode
{
  "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:
Contoh kode
{
  "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.statusdelivered, 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:
Contoh kode
{
  "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:
StatusMeaning
acceptedTaken for delivery.
processingThe navigator is working through its channels.
sentHanded to the platform.
sending_failedCould not be handed over. Triggers fallback.
deliveredReached the recipient. Terminal.
delivery_failedThe platform could not deliver it. Triggers fallback.

Failure codes worth recognising

CodeDescriptionIn a fallback chain
12003Unsupported destinationThe platform cannot reach this destination — an RCS agent not launched for the recipient's carrier or country. Falls back.
12006Invalid messageThe content is not valid for that platform, e.g. a platform needing a template. Falls back.
15016Recipient opted outThe recipient is suppressed. Ends the chain — no fallback.
14007Recipient in global holdoutEnds 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.