SMS Providers¶
Text messages are a natural last mile for database events — quota warnings, failed-job alerts, on-call nudges — and every serious SMS provider exposes exactly the kind of HTTP API the webhook transport was built for. Twelve providers have been assessed against the fit test; eleven fit, and the first adapter has shipped: Telnyx (p_provider => 'telnyx' — setup and usage in Resend, Telnyx, and PagerDuty), the cleanest fit of the twelve (bearer token, flat JSON, message id in the reply) and the template the rest will follow. The other rows remain assessments — not yet selectable at create_profile(). (Wire-level worked profiles for all eleven: pg_relay's SMS chapter.)
| Provider | Fits? | Auth shape | Body | Notes |
|---|---|---|---|---|
| Telnyx | ✔ shipped adapter | Bearer | JSON | The clean template: POST /v2/messages, 200 → data.id — use it |
| Twilio | ✔ | Basic (Account SID + token) | form | Form-encoded only — the case body_style: "form" exists for; SID appears in both URL and username by design |
| Bandwidth | ✔ | Basic | JSON | 202 → id |
| Plivo | ✔ | Basic (Auth ID + token) | JSON | 202 → message_uuid |
| Vonage | ✔ | Basic | JSON | Messages API (v1/messages); the legacy body-authenticating SMS API also works now via body_merge — but prefer Messages |
| Sinch | ✔ | Bearer | JSON | Batches endpoint, 201 → id |
| Infobip | ✔ | Custom header | JSON | Two quirks — see below |
| Telesign | ✔ | Basic | form | Content-Type needs a charset suffix via headers |
| Kaleyra | ✔ | Custom header (api-key) |
JSON | Thinnest documentation of the set |
| MessageBird / Bird | ✔ | Custom header (AccessKey …) |
JSON | Whole header value in the env var |
| Sendblue | ✔ | Two custom headers | JSON | Second secret header rides in profile.headers — _env: resolves there too |
| AWS SNS | ✘ excluded | SigV4 signing | — | Rolling per-request HMAC; no static-credential form exists. If AWS-committed: use SES for email, or front SNS with your own token-accepting proxy |
The doctrine an adapter must respect¶
These aren't provider trivia — they're the rules any SMS adapter in this extension will encode, and worth understanding before requesting one:
"Sent" means accepted, not delivered. Every provider reports final delivery through callback webhooks, and pg_relay deliberately has no inbound listener. A 2xx is terminal sent; the message id recorded as provider_ref is your handle for reconciling actual delivery in the provider's console. If your requirement is proof of delivery inside the database, SMS through this system cannot give it — decide that up front.
Phone numbers are message content. The same rule that keeps email addresses out of every log applies: numbers must never appear in classifier detail text, trigger-side logging, or anywhere outside the message itself. Providers sometimes echo numbers in error bodies — an adapter's classification branch has to strip or accept that deliberately, not by accident.
Trust the body, not just the status, where the provider requires it. Infobip answers 200 for accepted for processing and puts each message's real disposition in the body (messages[0].status.groupName — REJECTED means failed despite the 200). Its adapter must read the body; an HTTP-status-only classifier silently records rejected messages as sent. Infobip's auth is also an Authorization: App <key> scheme — custom_header carrying the whole value, not bearer_header.
Vonage closed an old exclusion. Its legacy SMS API authenticates inside the request body (api_key/api_secret) — historically ruled unusable, now served by the profile's body_merge overlay injecting both from the environment. The Messages API remains the recommended route; the point is that body-side auth no longer disqualifies anyone from this table.
Using the Telnyx adapter¶
The pattern mirrors Slack's: a webhook profile with p_provider => 'telnyx', the API key as _env:TELNYX_API_KEY, the destination number as the single recipient, the sending number as the sender (p_sender or the profile's send_from default — or a messaging_profile_id in p_payload instead), and the notification body as the text. The adapter builds Telnyx's {from, to, text} request and classifies 2xx → sent (data.id), 429/5xx/no-response → retry, other 4xx → failed with Telnyx's first error. Full setup, payload keys, and the delivery-receipt caveat: Resend, Telnyx, and PagerDuty. A profile-pinned sender number via body_merge works exactly like pinning a Slack channel.
Next: Incident Alerting Platforms — where at-least-once delivery meets APIs actually designed for it.