Skip to content

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, 200data.iduse 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 202id
Plivo Basic (Auth ID + token) JSON 202message_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, 201id
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.groupNameREJECTED 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.