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.

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 StatesEuropean Union
Sender shown10-digit long code, toll-free, or short codeAlphanumeric sender ID in most markets
RegistrationA2P 10DLC brand plus campaign, mandatoryPer-country, several markets require pre-registration
Opt-outSTOP must work; CTIA principles expect itWithdrawal of consent must be as easy as giving it
If you skip itFiltering and throttling, usually silentSender replaced with a number, or the message dropped
Extra obligationQuiet-hours rules vary by stateLawful 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.

References

Browse more sms developer guides