SMS provider¶
The third-party SMS gateway Beacon once sent text messages through. The SMS channel is effectively retired — no new SMS is sent today — but the gateway client and its delivery-receipt ingest still exist in the tree, and historical
channel: "sms"delivery events survive. Treat this page as the record of a deprecated dependency, not a live one. (Services that list it independs_onshow up under "Depended on by" — messaging-service still does.)
Read this first: SMS is retired
Across the docs, SMS is consistently described as a retired channel — see the messaging domain, the send flow, the delivery-event model, and the glossary. Email is the live, first-class channel; push rides the same path. SMS exists in the data model and on old records only. Everything below documents what the integration was and what's left of it, so that anyone touching the leftover code knows what they're looking at.
What we use it for¶
Historically: delivering SMS (text) messages on behalf of an organization, and
receiving delivery receipts (the carrier's confirmation that a message
reached, or failed to reach, a handset) back into Beacon's ingest. Those receipts
became delivery events with channel: "sms", the
same way email delivery/bounce callbacks do today — see the
email provider for the channel that's still live.
In the send flow, SMS was just another value of the channel knob in
message settings: an org could pick sms as its
default_channel or fallback_channel, and the
send path branched to this gateway instead of the
email provider. The shape of the flow was identical to email — render a
template, hand it to the provider, write a delivery event, emit the outcome — only
the provider on the far end differed.
Today that path is dormant. The model, the settings, and the historical events all
still mention SMS, but no live send reaches this gateway. If you're reading this to
understand a channel: "sms" row in the delivery log, the short answer is: it's a
historical record from when this integration was live.
Interface¶
The integration was a symmetric request/response-plus-callback shape, the same pattern Beacon uses for email:
- Direction (outbound) — Beacon
POSTs a message (destination number + rendered body) to the gateway's send API. The gateway responds synchronously with an accept/reject and a provider-side message identifier. - Direction (inbound) — the gateway
POSTs delivery receipts back to Beacon's ingest endpoint asynchronously, often seconds to minutes later once the carrier reports back. A receipt carries the provider message id and a terminal status, which Beacon mapped onto a delivery event (delivered/failed). - Auth & secrets — an account SID plus an auth token, held in the secrets manager and referenced by name only — never the values, and never in these docs. The same credential authenticated outbound sends and was used to verify inbound receipts.
- Environments — the gateway provides magic test numbers: special
destination numbers that deterministically simulate a
deliveredor afailedreceipt without actually sending an SMS or incurring carrier cost. This is the SMS analogue of the email provider's sandbox key that black-holes real delivery, and it's how integration tests exercised the receipt path safely.
sequenceDiagram
participant Messaging as messaging-service
participant Gateway as SMS gateway
participant Carrier as Mobile carrier
Messaging->>Gateway: POST send (number, body) + SID/token
Gateway-->>Messaging: 202 Accepted (provider_id)
Gateway->>Carrier: route to handset
Carrier-->>Gateway: delivery report
Gateway->>Messaging: POST delivery receipt (provider_id, status)
Messaging->>Messaging: write delivery_event (channel: sms)
Failure modes¶
How the gateway failed, and how Beacon handled each kind — the distinction that matters is terminal versus transient:
| Failure | Example | Beacon's handling |
|---|---|---|
| Carrier rejection (terminal) | invalid number, recipient opted out (STOP) | recorded as a terminal failed delivery event — not retried |
| Transient gateway error | gateway 5xx, timeout, rate-limit |
retried per the org's retry policy; exhausted retries become a failed event |
| Receipt never arrives | carrier silent | the send stays at sent — confirmation can lag or never come, exactly as with email |
Two points worth holding onto:
- A rejection is a real, recorded outcome, not a silent drop. An invalid
number or an opt-out lands a terminal
faileddelivery event and, for live sends, amessage.failedwebhook. Don't retry these — the recipient genuinely cannot or will not receive the message, and re-sending to an opted-out number is a compliance problem, not a transient blip. - Transient errors follow the same retry machinery as email. The
retry_policyoption (none/standard/aggressive) governed SMS retries identically;fallback_channelonly fired after the primary's retries were exhausted, so an SMS-primary org with a retry policy ofnonenever fell back at all.
How a send reached this gateway¶
For context, the SMS path slotted into the standard messaging send flow:
- A send is triggered (API, dashboard, or an internal event) with
channel: smseither explicitly or via the org'sdefault_channel. - messaging-service checks the org's status
against accounts-service — a
suspendedorg is dropped before any provider is touched — then renders the template from PostgreSQL. - The service branches on channel and hands the rendered message to this gateway.
- The gateway's response, then later its delivery receipt, are written as
delivery events in MongoDB and surfaced as
message.*webhooks.
The only step unique to SMS was step 3's branch to this gateway instead of the email provider. That branch is the code most likely to be dead now — see below.
Notes & nuances¶
Number pooling (historical)
Outbound numbers were pooled per region: Beacon sent from a rotating set of
sender numbers chosen by the destination's region rather than a single fixed
number. Deliverability therefore varied by destination country — some carriers
and countries are far stricter about pooled / shared numbers — which made SMS
inherently less predictable than email. This is one of the reasons SMS was a
harder channel to operate, and useful background if you're interpreting the
spread of outcomes in old channel: "sms" delivery events.
Relationship to the delivery-event model
Every SMS attempt wrote a delivery event just
like email does. Because that collection is schemaless and write-once, old
SMS rows carry only the fields that existed when they were written — many
predate provider_id (≥ 2024-03), region (≥ 2024-07), and an explicit
attempts (treat absent as 1). If you query historical SMS, default these
defensively; the model page lists every cutoff.
Discrepancy: page status vs. retired channel
This page's frontmatter still reads status: active, but every other source
(the messaging domain, the
send flow, the
delivery-event model, the
glossary, and the RabbitMQ datastore)
describes SMS as retired with no new sends. The status field looks stale and
is a candidate to flip to deprecated/inactive on the next review — flagged
here rather than silently changed.
Suspected dead
The SMS-gateway code paths appear unreachable in current operation:
- messaging-service carries a
legacy.sms_gatewayconsumer wired to a RabbitMQ queue with no producers since SMS was retired (also flagged on the RabbitMQ datastore). - The send flow still branches on
channel: "sms"to select this legacy gateway, but no live send sets that channel, so the branch looks unreachable. - No new
channel: "sms"delivery events are being written; only historical rows remain.
Confirm before removing — historical reporting still reads the old SMS delivery events, so the data must stay even if the integration code is removed.
Related pages¶
Depended on by: messaging-service
Confidence: 60/100 · Last reviewed: 2026-06-14 · Status: active
Verified against: beacon-messaging@7a8b9c0 (2026-05-22)