SMS alerts with a registered sender: the compliance path, end to end
Sender ID registration, consent and STOP handling, then delivery tracking — the whole alerting workflow as API calls, with the US and EU rules that differ.
Shipping SMS alerts is four jobs, not one: register the identity you send under, record consent, honour opt-outs, and track what happened to each message. On Infrai the first and third are API calls rather than support tickets — POST /v1/sms/signature/create submits the sender for review, and the suppression list holds every number that said STOP. The send itself is the easy part.
Registration is the long pole. Everything else fits around it.
A startup that writes the send call on day one and discovers registration on day thirty has just added weeks to a launch, because carrier review runs on its own clock and nothing you do speeds it up.
Submit the sender identity first
export INFRAI_API_KEY="your_infrai_api_key"
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": "KbAlerts",
"type": "company",
"proof_url": "https://example.com/business-license.pdf"
}'
{
"ok": true,
"data": {
"signature_id": "smssig_S72nd3KEbSDOaq0v09ST",
"name": "KbAlerts",
"type": "company",
"proof_url": "https://example.com/business-license.pdf",
"review_state": "pending",
"created_at": "2026-07-26T01:07:10.157564Z"
}
}
review_state starts at pending and you poll it. That’s a real response from a live account, and the state it lands in is the one your launch plan has to respect:
curl -sS "https://api.infrai.cc/v1/sms/signature/get/smssig_S72nd3KEbSDOaq0v09ST" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
The read is free and rate-limited, so a nightly job that alerts your team when a signature flips to approved or rejected costs nothing. proof_url should point at a document a reviewer can open without a login — a business registration certificate or a page on your own domain showing the brand name.
The template gate, and a 402 worth knowing about
Some destinations won’t accept free-form bodies at all and require an approved template. POST /v1/sms/template/create is the route, but there’s a plan boundary in front of it that the capability catalogue doesn’t advertise: on a standard account it answers HTTP 402.
{
"ok": false,
"error": {
"code": "PRO_REQUIRED",
"http_status": 402,
"message": "custom SMS template creation is Pro-only; standard accounts cannot submit templates for vendor review",
"retryable": false
}
}
We hit that submitting a template on a live standard key. If your alert traffic targets a market that mandates templates, budget for the plan upgrade in the same sprint as registration — discovering it late is the same schedule problem as discovering registration late.
Consent, STOP, and the list that enforces both
Under GDPR a phone number is personal data and a marketing-flavoured alert needs a lawful basis recorded before you send. In the US, CTIA’s messaging principles make opt-out handling non-negotiable and expect STOP to work on every campaign. Both collapse into one operational rule: check the suppression list before every send, and never route around it.
curl -sS -X POST "https://api.infrai.cc/v1/sms/suppression/check" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"phone": "+14155550142"}'
{
"ok": true,
"data": { "phone": "+14155550142", "suppressed": false }
}
Two obligations that aren’t in the API and are yours regardless: quiet hours, since several US states restrict messaging outside roughly 08:00–21:00 local time, and retention, since a number you keep for alerts is a number you have to delete on request.
The dispatcher, in Python 3
# alerts.py — suppression preflight, send, then a delivery read.
# Run: INFRAI_API_KEY=your_infrai_api_key python3 alerts.py
import os
import sys
import time
import requests
BASE = "https://api.infrai.cc"
KEY = os.environ.get("INFRAI_API_KEY")
if not KEY:
sys.exit("INFRAI_API_KEY is not set")
HEADERS = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}
SENDER = "KbAlerts"
def unwrap(res):
payload = res.json()
if payload.get("ok") is False:
err = payload["error"]
raise RuntimeError(f"{err['code']}: {err['message']}")
return payload["data"]
def is_suppressed(phone):
res = requests.post(
f"{BASE}/v1/sms/suppression/check",
headers=HEADERS,
json={"phone": phone},
timeout=15,
)
return unwrap(res)["suppressed"]
def send_alert(phone, text):
res = requests.post(
f"{BASE}/v1/sms/send",
headers=HEADERS,
json={"to": phone, "body": text, "from": SENDER},
timeout=30,
)
return unwrap(res)
def final_state(message_id, tries=6, gap=20):
for _ in range(tries):
res = requests.get(f"{BASE}/v1/sms/status/{message_id}", headers=HEADERS, timeout=15)
data = unwrap(res)
if data.get("state") in ("delivered", "failed", "expired", "cancelled"):
return data
time.sleep(gap)
return {"state": "pending"}
def main():
phone = "+14155550142"
if is_suppressed(phone):
print(f"{phone} opted out; skipping")
return
sent = send_alert(phone, "Disk at 91% on db-primary. Runbook: ops/disk.")
print(f"queued {sent['message_id']} segments={sent['segments']} cost={sent['cost_usd']}")
print("final:", final_state(sent["message_id"]))
if __name__ == "__main__":
main()
Note the segment count in that print. Billing counts segments, not sends, and an alert body with an accented character silently drops to a 70-character segment — so a message you priced as one becomes three. Keep alert copy ASCII and short.
US and EU, where the rules actually diverge
| United States | European Union | |
|---|---|---|
| Sender shown | 10-digit long code, toll-free, or short code | Alphanumeric sender ID in most markets |
| Registration | A2P 10DLC brand plus campaign, mandatory | Per-country, several markets require pre-registration |
| Opt-out | STOP must work; CTIA principles expect it | Withdrawal of consent must be as easy as giving it |
| If you skip it | Filtering and throttling, usually silent | Sender replaced with a number, or the message dropped |
| Extra obligation | Quiet-hours rules vary by state | Lawful basis and retention limits under GDPR |
The asymmetry is worth internalising. US failures are mostly technical and silent; EU failures are mostly legal and loud.
What an alert costs to run
curl -sS "https://api.infrai.cc/v1/discovery" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" | head -c 300
curl -sS "https://api.infrai.cc/v1/account/usage" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
Verified 2026-07-26 on Infrai: $0.007475 per message, and every supporting call in this workflow — signature create, signature read, suppression check, status read — is free and rate-limited. So a service sending 2,000 alerts a month spends about $15 and pays nothing for the compliance machinery around it. New accounts get $2 free, roughly 267 messages. These rates tend to fall rather than rise, so GET /v1/discovery is the number to trust over this page.
Where a specialist is the better buy
If alerts are your product — paging, incident routing, on-call escalation with acknowledgements — you’d be better off with a purpose-built platform, and if you need short codes, dedicated number pools or carrier escalation paths, Twilio and Sinch sell that depth and a general gateway won’t match it. The honest limits here: SMS is served in the western region with Tencent as the ready vendor and Twilio still pending, there’s no outbound delivery webhook so tracking is polling, GET /v1/sms/inbound/list needs a hydrated vendor key before it returns anything but a 503, and template submission is Pro-only. What you get in exchange is that the alert, the email that duplicates it, the cron job that schedules it and the error tracker that caught the condition all sit behind one key and one bill.