> ## Documentation Index
> Fetch the complete documentation index at: https://docs.robase.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Providers & failover

> How we route a send across Beem, Termii, and direct SMPP binds.

Robase sits in front of multiple upstream SMS providers. You never pick a provider — we route based on the destination country and the ordered **failover chain**.

## Current providers

| Provider   | Primary markets                   | Failover role                         |
| ---------- | --------------------------------- | ------------------------------------- |
| **Beem**   | All supported countries (default) | Primary everywhere                    |
| **Termii** | Nigeria                           | NG-specific fallback for Beem outages |

We add providers as markets demand. On the Scale plan, you can request a specific provider preference (e.g. direct SMPP bind to MTN) — contact support.

## How routing works

<Steps>
  <Step title="Country detected">
    From the `to` number's E.164 dial code.
  </Step>

  <Step title="Chain resolved">
    We look up `provider_routes` for that country plus the `*` global defaults. Country-specific rows go first; `*` rows fill the tail.
  </Step>

  <Step title="Walk the chain">
    The worker tries each provider in order. Success → persist and fire `sms.sent`.
  </Step>

  <Step title="Failover on transient errors">
    5xx responses, timeouts, network errors → try the next provider. Every attempt is recorded.
  </Step>

  <Step title="Short-circuit on permanent errors">
    `phone_invalid` / `dnd_blocked` — failover wouldn't help, and re-hitting the chain could look like spam. We stop immediately and mark rejected.
  </Step>
</Steps>

## Attempt history

Every send carries a `provider_attempts` array showing what we tried:

```json theme={null}
{
  "id": "01H8XKQJ3Z...",
  "status": "delivered",
  "provider": "termii",
  "provider_message_id": "tm-a1b2c3",
  "provider_attempts": [
    {
      "provider": "beem",
      "attempted_at": "2026-04-17T10:30:01.234Z",
      "ok": false,
      "error_message": "upstream 502"
    },
    {
      "provider": "termii",
      "attempted_at": "2026-04-17T10:30:01.501Z",
      "ok": true,
      "provider_message_id": "tm-a1b2c3"
    }
  ]
}
```

Use this for postmortems on delivery incidents ("which provider actually delivered?") and to validate that failover is doing its job.

## Permanent vs transient errors

We classify errors so failover is smart, not noisy:

| Error                 | Category          | Behavior                                  |
| --------------------- | ----------------- | ----------------------------------------- |
| `phone_invalid`       | Permanent         | Short-circuit — don't try other providers |
| `dnd_blocked`         | Permanent         | Short-circuit                             |
| HTTP 5xx              | Transient         | Try next provider                         |
| Network timeout       | Transient         | Try next provider                         |
| HTTP 4xx (unexpected) | Provider-specific | Log as attempt failure, continue          |

## Provider-specific webhook endpoints

Each provider has its own DLR webhook endpoint we expose:

| Provider | Endpoint           | Signature header                            |
| -------- | ------------------ | ------------------------------------------- |
| Beem     | `POST /beem/dlr`   | `X-Beem-Timestamp` + `X-Beem-Signature`     |
| Termii   | `POST /termii/dlr` | `X-Termii-Timestamp` + `X-Termii-Signature` |

In production both require a shared HMAC secret (`BEEM_DLR_SECRET` / `TERMII_DLR_SECRET`). Timestamps older than 5 minutes are rejected as replays.

You don't interact with these directly — they're for the upstream providers to POST to. Your webhook (configured via `/v1/webhooks`) receives the correlated `sms.delivered` / `sms.failed` events.

## Adding a direct SMPP bind

For very high-volume customers (>1M SMS/month per country), a direct SMPP bind to the MNO delivers 10–20% faster and cheaper than going through an aggregator. Contact **[sales@robase.dev](mailto:sales@robase.dev)** — we'll set up the bind and route your traffic through it automatically while keeping failover to Beem/Termii for redundancy.
