Picking an SMS alerts provider for reminders, shipping and logins
Appointment reminders, shipping updates and account-activity alerts stress a provider differently. The four axes that decide it, with runnable US and EU examples.
Score providers on four axes and the shortlist gets short fast: what unit they bill, how they let you register a sender, whether opt-outs are a first-class list you can query, and what a delivery receipt costs to read. Infrai’s SMS surface answers all four over plain REST — free suppression and status reads, one billable send, sender and template registration as ordinary calls — which suits a team whose alerts are a supporting feature rather than the product.
Feature lists are a poor guide here because the three common alert families don’t want the same things. A reminder is scheduled days ahead and can tolerate a minute of latency; a login alert is worthless if it’s late; a shipping batch is 4,000 messages at 06:00 and nothing for the rest of the day. Infrai handles all three from one key, but the parts of the API you lean on differ.
What each family actually demands
| Alert family | Cadence | Opt-out exposure | The route that carries it |
|---|---|---|---|
| Appointment reminders | Scheduled, low volume, high personalisation | High — recipients reply STOP most often here | POST /v1/sms/send per recipient |
| Shipping and delivery | Bursty, high volume, templated | Medium | POST /v1/sms/batch/send, up to 100 per call |
| Account activity and logins | Event-driven, latency-sensitive | Low, but legally you still honour it | POST /v1/sms/send, then poll status |
Reminders are where the compliance work lives, so start there.
Suppression before send, not after complaint
Someone who replied STOP to last month’s reminder must not get this month’s. That’s a list lookup, and it should run inside the same function that builds the message — not as a nightly reconciliation.
export INFRAI_API_KEY="your_infrai_api_key"
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": "+15005550001"}'
{
"ok": true,
"data": { "phone": "+15005550001", "suppressed": false },
"metadata": { "request_id": "req_182ee2f11d6e430d942fd81f", "cost_usd": 0.0 }
}
Free, and fast enough to sit in the hot path — we measured 51 ms round-trip from a Western edge in July 2026. Additions go in through POST /v1/sms/suppression/add, and the full list is readable with GET /v1/sms/suppression/list when an auditor asks.
Now the caveat that decides whether this is enough for you. The supported opt-out path is the suppression list you write to directly — capture a STOP however it reaches you (a web unsubscribe page, a support-inbox rule, a keyword handled on the number upstream) and record it with POST /v1/sms/suppression/add, which then gates every later send. What Infrai doesn’t do is run an inbound-keyword automation that files those replies for you, so if two-way STOP parsing on the number is central to your product, that’s a reason to look elsewhere today.
Senders and templates: check what you already have
curl -sS "https://api.infrai.cc/v1/sms/signature/list" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
That returns your registered signatures with a review_state — pending until a carrier signs off, which for China routes is mandatory before a single message goes out. For US and EU sends you pass the copy inline as body and skip templates entirely.
Custom template registration has a hard boundary worth knowing before you design around it:
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": "appointment_reminder_en",
"body": "Reminder: your appointment is {when} at {place}.",
"locale": "en-US",
"variables": ["when", "place"]
}'
Submitting templates for carrier review sits on the Pro tier, and it’s declared rather than discovered the hard way: GET /v1/discovery/sms.template.create publishes minimum_tier: "pro", so a standard key gets a clean 402 and your deployment check can assert the tier before launch. It only bites if you need carrier-approved templates (China, or an EU operator that insists). Keep your reminder copy inline in body and the point is moot.
A reminder worker, end to end
// remind.mjs — Node 22, ESM. Sends tomorrow's appointment reminders.
const API = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("set INFRAI_API_KEY");
async function call(path, init = {}) {
const res = await fetch(`${API}${path}`, {
...init,
headers: {
authorization: `Bearer ${KEY}`,
"content-type": "application/json",
...(init.headers ?? {}),
},
});
const payload = await res.json();
if (!res.ok) {
const err = new Error(payload?.error?.message ?? `HTTP ${res.status}`);
err.code = payload?.error?.code;
err.requestId = payload?.error?.request_id;
throw err;
}
return payload.data;
}
export async function sendReminders(appointments, senderId = "AcmeCare") {
const eligible = [];
for (const appt of appointments) {
const screen = await call("/v1/sms/suppression/check", {
method: "POST",
body: JSON.stringify({ phone: appt.phone }),
});
if (screen.suppressed) continue;
eligible.push({
to: appt.phone,
body: `Reminder: ${appt.when} with ${appt.clinician}. Reply STOP to opt out.`,
from: senderId,
});
}
if (!eligible.length) return { sent: 0, results: [] };
const batch = await call("/v1/sms/batch/send", {
method: "POST",
body: JSON.stringify({
messages: eligible,
idempotency_key: `reminders-${new Date().toISOString().slice(0, 10)}`,
}),
});
return { sent: eligible.length, results: batch.results };
}
The idempotency_key is the part people skip and then regret. A reminder job that times out mid-flight and gets retried by your scheduler will otherwise text every patient twice, and a duplicate medical reminder generates a support call and an opt-out in the same afternoon.
Ninety minutes later, sample the outcomes:
curl -sS "https://api.infrai.cc/v1/sms/status/msg_01JR4K7T2ZQ8M0YB5X3NDCV9WE" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
state, attempt, delivered_at and failed_reason come back, all for free. You don’t need a webhook endpoint to run a reminder product — polling a few hundred ids after a batch is cheaper in code than owning a public callback URL.
The cost shape, and today’s number
Sends are the only metered part. Read on 2026-09-21:
POST /v1/sms/send— $0.008395, unitper_messagePOST /v1/sms/batch/send— $0.008395, unitper_message, up to 100 recipients per call
Both are marked approximate because the vendor mix moves underneath — and note that these two rates are currently the same, where an earlier reading of this page had the batch route lower. That’s the whole argument for reading rates live rather than trusting a figure in an article: the relationship between two prices is no more permanent than the prices.
What does survive a repricing is the operational case for batching. One call for a hundred reminders is one round trip instead of a hundred, one rate-limit interaction instead of a hundred, and one response to reconcile — so batch a reminder run because it is simpler and better behaved, not because you were promised a discount. Prices in this market keep drifting down, so read them live:
curl -sS "https://api.infrai.cc/v1/account/balance" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
| jq '{balance_usd, runway_days, sms: .affordable_uses_hint["sms.send"]}'
affordable_uses_hint answers the question a finance lead actually asks — how many more messages does the current balance buy — without a spreadsheet.
Where a specialist beats this
Twilio is the better pick if you need scheduled sends managed on their side, Messaging Services with per-country sender pools, or the deepest carrier-error taxonomy in the market; its evaluation guides and the round-ups at textmagic cover that ground fairly. Plivo undercuts it on published per-segment rates and is worth a quote if SMS volume is your dominant cost line.
The trade-off runs the other way for a small team, and it shows up on the finance side rather than the engineering one. Every message this worker sends, plus the POST /v1/cron/create job that wakes it, plus the storage holding the appointment export, lands in a single GET /v1/account/usage response with a per-capability breakdown. So “what did the Northgate clinic cost us in July” is one query against one bill — not a reconciliation across four vendors’ invoices with four different billing periods and four definitions of a message. That’s the axis a specialist SMS API can’t compete on, because it only ever sees the SMS line.