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. Automatic capture of STOP replies depends on inbound message support, and GET /v1/sms/inbound/list currently returns VENDOR_NOT_CONFIGURED on a standard account — the route is published, the vendor side isn’t live yet. Until it is, you’re wiring opt-outs from whatever channel you already own (a web unsubscribe page, a support inbox rule) into POST /v1/sms/suppression/add yourself. If inbound keyword handling 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"]
}'
On a standard account that answers HTTP 402, PRO_REQUIRED — submitting templates for vendor review is a paid-plan capability. It’s a limitation, not a bug, and it only bites if you need carrier-approved templates (China, or an EU operator that insists). Keep your reminder copy in your own code 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. POST /v1/sms/send and POST /v1/sms/batch/send bill $0.007475 per message, verified 2026-07-26 and marked approximate because the vendor mix moves underneath. New accounts get $2 in free credit, about 267 messages. Prices in this market keep drifting down, so read it live rather than trusting a figure in any article, this one included:
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. One credential here also reaches email, cron, queues and error tracking, so the reminder scheduler, the retry queue and the failure alert don’t each arrive with their own account, key rotation and invoice — and per-tenant cost attribution is a query against one usage view instead of a reconciliation across four vendors.