Skip to main content
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

ProviderPrimary marketsFailover role
BeemAll supported countries (default)Primary everywhere
TermiiNigeriaNG-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

1

Country detected

From the to number’s E.164 dial code.
2

Chain resolved

We look up provider_routes for that country plus the * global defaults. Country-specific rows go first; * rows fill the tail.
3

Walk the chain

The worker tries each provider in order. Success → persist and fire sms.sent.
4

Failover on transient errors

5xx responses, timeouts, network errors → try the next provider. Every attempt is recorded.
5

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.

Attempt history

Every send carries a provider_attempts array showing what we tried:
{
  "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:
ErrorCategoryBehavior
phone_invalidPermanentShort-circuit — don’t try other providers
dnd_blockedPermanentShort-circuit
HTTP 5xxTransientTry next provider
Network timeoutTransientTry next provider
HTTP 4xx (unexpected)Provider-specificLog as attempt failure, continue

Provider-specific webhook endpoints

Each provider has its own DLR webhook endpoint we expose:
ProviderEndpointSignature header
BeemPOST /beem/dlrX-Beem-Timestamp + X-Beem-Signature
TermiiPOST /termii/dlrX-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 — we’ll set up the bind and route your traffic through it automatically while keeping failover to Beem/Termii for redundancy.