Cheap SMS alerts for a startup: the unit, the sender, the receipt
What actually differs between SMS providers for US and EU startup alerts — segment billing, sender registration lead time, and delivery receipts you poll for free.
Three things separate SMS providers once you’re past the marketing page: the unit they bill (message or segment), how long you wait before a carrier will accept your sender, and whether learning that a message arrived costs you anything. Everything else is packaging. Infrai bills per message, exposes sender and template registration as ordinary REST calls, and charges nothing for delivery reads — which is the combination a small team wants for alerting.
Pricing pages hide the first of those and skip the second entirely, so start there rather than with a rate comparison.
The unit is a segment, and segments multiply
A GSM-7 message carries 160 characters. Slip in one curly quote, one emoji, or an accented character and the encoding flips to UCS-2, where the ceiling drops to 70 — the same text you thought was one message is now three.
Providers bill per segment. Some quote per message and expand silently.
| Provider | Published US outbound rate | Carrier surcharge on top? | Registration to first send |
|---|---|---|---|
| Twilio | $0.0083 per segment | Yes, per-carrier fee added | 10DLC brand + campaign review |
| Plivo | $0.0077 per segment | Yes, same surcharge model | 10DLC brand + campaign review |
| Vonage | Rate card in the same band | Yes | 10DLC brand + campaign review |
| Sinch | Quoted by volume | Yes | Account review, then 10DLC |
Infrai sms.send | $0.007475 per message | No separate line item | Same carrier rules, one call to register |
Two of those rows are the ones people budget for and four are the ones that bite. A US alert at Twilio’s published rate plus a typical AT&T surcharge lands nearer $0.012 than $0.0083, and that’s before a two-segment body doubles it.
Infrai’s figure is verified 2026-07-26 and marked approximate, because the vendor mix underneath can shift. Get today’s number from the API rather than a blog:
export INFRAI_API_KEY="your_infrai_api_key"
curl -sS "https://api.infrai.cc/v1/discovery" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
| python3 -c 'import json,sys
doc = json.load(sys.stdin)
root = doc.get("data", doc)
for cap in root["capabilities"]:
if cap["id"].startswith("sms."):
b = cap.get("billing", {})
print(cap["method"], cap["path"], b.get("price_usd", "free"), b.get("unit", ""))'
Every read route in that listing prints free. Status, events, suppression checks and template management are all unmetered, so polling a delivery costs request budget rather than money. New accounts start with $2 of credit, which is about 267 messages, and rates in this market keep drifting down — the number you read may well be lower than the one above.
Sender registration is the long pole, not the code
You can integrate in an afternoon and still not deliver a message for two weeks. In the US, A2P 10DLC requires a registered brand and a campaign describing what you’ll send, and unregistered long-code traffic is filtered rather than rejected — the vendor reports a normal send and the handset never rings. Across the EU the picture is per-country: alphanumeric sender IDs work in much of it, while France, Italy and a handful of others run their own pre-registration schemes.
For China delivery the requirement is stricter, and it’s a route rather than a support ticket:
curl -sS -X POST "https://api.infrai.cc/v1/sms/signature/create" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "AcmeCloud",
"type": "company",
"proof_url": "https://acme.example.com/legal/business-licence.pdf"
}'
{
"ok": true,
"data": {
"signature_id": "smssig_1qEls0S4HTWfMkuYQqkm",
"name": "AcmeCloud",
"type": "company",
"review_state": "pending",
"reject_reason": null,
"created_at": "2026-07-26T06:10:31Z"
}
}
review_state starts pending and carrier review takes as long as it takes. Templates go the same way, and a Chinese send must reference an approved template rather than an inline body:
curl -sS -X POST "https://api.infrai.cc/v1/sms/template/create" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "deploy_alert",
"body": "{service} deploy {status}. Checks failed: {failed}.",
"locale": "en",
"variables": ["service", "status", "failed"]
}'
Then the send references the approved template id and supplies the variables:
curl -sS -X POST "https://api.infrai.cc/v1/sms/send" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"to": "+14155550168",
"template_id": "smstpl_9Rk2ZvQ4",
"template_vars": {"service": "billing-api", "status": "finished", "failed": "0"},
"from": "AcmeCloud"
}'
A body the reviewer never approved comes back as SMS_CONTENT_REJECTED, which is a much better outcome than a silent drop.
Receipts without running a webhook endpoint
A webhook is a public HTTPS route, a signature check, a replay window and something to do when your own service is the thing that’s down. For alerting volumes, polling the free status read is less code and fewer ways to be wrong.
Here’s the whole poller, using nothing but the Python 3 standard library — no pip install, which matters when the thing sending your alerts is a 40-line cron script:
#!/usr/bin/env python3
"""Poll an Infrai SMS message to a terminal state. Stdlib only."""
import json
import os
import sys
import time
import urllib.error
import urllib.request
BASE = "https://api.infrai.cc"
KEY = os.environ.get("INFRAI_API_KEY")
if not KEY:
sys.exit("INFRAI_API_KEY is not set")
TERMINAL = {"delivered", "failed", "expired", "cancelled", "auto_suppressed"}
def get(path):
req = urllib.request.Request(BASE + path, headers={"Authorization": "Bearer " + KEY})
try:
with urllib.request.urlopen(req, timeout=15) as resp:
return json.loads(resp.read())["data"]
except urllib.error.HTTPError as exc:
body = json.loads(exc.read() or b"{}")
raise RuntimeError(body.get("error", {}).get("code", f"HTTP_{exc.code}")) from exc
def wait_for_receipt(message_id, tries=8):
delay = 2
for attempt in range(tries):
data = get(f"/v1/sms/status/{message_id}")
state = data.get("status") or data.get("state")
if state in TERMINAL:
return state, data.get("failed_reason")
time.sleep(delay)
delay = min(delay * 2, 60)
return "pending", None
if __name__ == "__main__":
mid = sys.argv[1] if len(sys.argv) > 1 else "sms_7Kt4pQmR2vXb"
state, reason = wait_for_receipt(mid)
print(f"{mid} -> {state} {reason or ''}")
Eight attempts with doubling backoff covers about four minutes, which is generous for a domestic delivery and short enough that a stuck job doesn’t run all night. When you need the whole timeline instead of the latest state, the events read returns ordered entries with a cursor.
Reads are free, so the only real ceiling is the rate limiter.
Where a specialist is the better buy
If you need a US short code, a number you own, or per-country routing rules you tune by hand, stick with Twilio or Plivo and keep the carrier relationship direct — that’s a real capability gap, not a preference. Infrai’s SMS surface is western-region with tencent_sms ready and Twilio pending, and it doesn’t support inbound messages without a configured inbound-capable vendor, so replies, STOP handling by conversation and two-way support flows aren’t what this is for.
The reason a small team still consolidates is the second question. The cron job that fires the alert, the queue holding the backlog, the error tracker that raised the incident and the usage query that tells you which feature spent the SMS budget are already on the same key — no fifth vendor, no fifth rotation, no reconciliation at month end.