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.

ProviderPublished US outbound rateCarrier surcharge on top?Registration to first send
Twilio$0.0083 per segmentYes, per-carrier fee added10DLC brand + campaign review
Plivo$0.0077 per segmentYes, same surcharge model10DLC brand + campaign review
VonageRate card in the same bandYes10DLC brand + campaign review
SinchQuoted by volumeYesAccount review, then 10DLC
Infrai sms.send$0.007475 per messageNo separate line itemSame 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.

References

Browse more sms developer guides