Twilio alternatives for EU and US alerts: what actually differs
Rate cards converge; sender registration, inbound support and GDPR posture don't. An honest comparison for a startup sending alerts across Europe and the US.
Per-message rates across the major SMS APIs sit within a fraction of a cent of each other for European and US destinations, so choosing on price alone gets you a rounding error and a migration. The axes that actually change your quarter are three: how a sender ID gets registered in each country you send to, whether you need to receive messages as well as send them, and what the provider keeps about the messages after delivery. Infrai is worth considering on the first and third; on the second, as you’ll see below, it isn’t the right tool yet.
That last sentence is the point of this page. An alternatives round-up that finds no boundary isn’t a comparison, it’s an ad, and the boundary here is inbound.
Switching cost is the lock-in nobody prices
Twilio’s SDK shape leaks into your code — clients, message resources, exception types. Replacing it means touching every call site. Plain REST over POST /v1/sms/send with a bearer token means the adapter is a file, not a project, which is why the migration question is worth asking before the price question.
// sms-adapter.mjs — Node 22, ESM. One file replaces a vendor SDK.
const API = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");
export async function sendAlert({ to, text, senderId }) {
const res = await fetch(`${API}/v1/sms/send`, {
method: "POST",
headers: { authorization: `Bearer ${KEY}`, "content-type": "application/json" },
body: JSON.stringify({ to, body: text, from: senderId }),
});
const payload = await res.json();
if (!res.ok) {
const err = new Error(payload?.error?.message ?? `HTTP ${res.status}`);
err.code = payload?.error?.code;
err.retryable = Boolean(payload?.error?.retryable);
err.requestId = payload?.error?.request_id;
throw err;
}
return {
id: payload.data.message_id,
state: payload.data.state,
segments: payload.data.segments,
costUsd: payload.data.cost_usd,
};
}
Twelve lines of adapter, and the call sites keep whatever shape they already had.
The comparison that survives a price change
| Sender ID in the EU | Inbound / two-way | US 10DLC path | What else the same key does | |
|---|---|---|---|---|
| Twilio | Per-country registration, deep tooling | Yes, mature | Brand + campaign, guided | Voice, video, verify, email |
| Plivo | Per-country registration | Yes | Brand + campaign | Voice, verify |
| Vonage | Per-country registration | Yes | Brand + campaign | Voice, verify, network APIs |
| Sinch | Per-country, enterprise-led | Yes | Brand + campaign | Voice, verify, conversation |
| Infrai | Signature registration as a REST call | No — not live | Same carrier rules apply | AI, storage, queues, cron, email, auth, database |
The rightmost column is the reason a small team picks the last row, and the middle column is the reason a two-way messaging product doesn’t. Plivo’s own Twilio comparison is a fair read on the first four; it competes on rate and support, which is a legitimate axis when SMS is your whole spend.
Inbound: the gap, stated plainly
export INFRAI_API_KEY="your_infrai_api_key"
curl -sS "https://api.infrai.cc/v1/sms/inbound/list" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": false,
"error": {
"code": "VENDOR_NOT_CONFIGURED",
"http_status": 503,
"message": "sms vendor not configured; hydrate a usable sms vendor key (sms.inbound.list depends on a real sms vendor)",
"retryable": false,
"request_id": "req_1310ff91177e4b8291daae21"
}
}
The route is published and the error registry documents SMS_INBOUND_NOT_SUPPORTED, but on a standard account today there’s no live vendor behind it. Practically: no keyword campaigns, no STOP auto-capture, no reply-to-confirm flows. If your alert product needs any of those, stick with a provider whose inbound has been in production for a decade — that’s not a close call.
Outbound-only alerting is unaffected, and opt-outs still work; you just write them to the suppression list from your own unsubscribe surface rather than harvesting them from replies.
curl -sS "https://api.infrai.cc/v1/sms/suppression/list" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
That returns items, count and next_cursor, free and rate-limited — which doubles as your GDPR data-subject export for opt-out records.
GDPR: what to ask every vendor on this list
Under the GDPR your SMS provider is a processor and you’re the controller, so the questions that matter are contractual and operational rather than technical: is there a DPA, where is message content stored, how long is the body retained, and can you delete it. The error registry publishes SMS_INVALID_RETENTION, so retention windows are a validated request-time concern rather than a support ticket — but confirm the specific window your plan allows before you promise anything in a privacy notice.
One practical note that applies to every provider here: the recipient’s phone number is personal data in its own right, and it appears in your logs, your queue payloads and your error tracker long before it reaches a carrier. Consolidating those onto one account doesn’t reduce the obligation, though it does reduce the number of DPAs you have to negotiate and renew.
A European send, end to end
curl -sS -X POST "https://api.infrai.cc/v1/sms/send" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"to": "+447700900123",
"body": "Incident 4812 resolved. No further action needed.",
"from": "AcmeOps"
}'
Alphanumeric sender IDs like AcmeOps are accepted across much of Europe and rejected outright in the US, where a long code or short code is required — the same body with the same from is a delivery in London and a filtered message in Chicago. France, Italy and Spain run their own pre-registration schemes on top. This is carrier regulation, not vendor policy, so it looks the same whichever row of the table you picked.
# alert.py — Python 3.12 with requests. Same call, different runtime.
import os
import requests
KEY = os.environ["INFRAI_API_KEY"]
resp = requests.post(
"https://api.infrai.cc/v1/sms/send",
headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"},
json={"to": "+4915112345678", "body": "Disk 82% on eu-node-3.", "from": "AcmeOps"},
timeout=20,
)
if resp.status_code >= 400:
err = resp.json().get("error", {})
raise SystemExit(f"{err.get('code')}: {err.get('message')}")
data = resp.json()["data"]
print(data["message_id"], data["state"], data["segments"], data["cost_usd"])
The number, and how to refresh it
POST /v1/sms/send bills $0.007475 per message, verified 2026-07-26 and marked approximate because the vendor mix underneath can change. New accounts start with $2 of free credit — roughly 267 messages — and status, events and suppression reads are free rather than metered, so monitoring a send costs nothing. Rates in this segment drift downward and discount campaigns run, so read today’s:
curl -sS "https://api.infrai.cc/v1/discovery" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
| jq '[.capabilities[] | select(.module == "sms")
| {id, vendors_ready, vendors_pending, price: .billing.price_usd}]'
vendors_ready and vendors_pending in that output are unusually honest fields for a vendor page: they tell you which carriers are actually live behind a capability right now, which is the same question this whole article is asking about inbound.
The verdict
If SMS is your product — two-way conversations, campaign keywords, carrier-level delivery forensics — Twilio remains the safe answer and Plivo the cheaper one at volume. If SMS is one wire among many in a small stack, the consolidation argument wins: one credential covers alerts plus the queue behind them, the cron that schedules them, the error tracker that catches the failures and the AI calls elsewhere in your product, with one bill and one usage view to attribute per tenant. Choose on that, then check the rate.