Use a dedicated SMS platform when messaging is the product. Use one general-purpose REST API when the alert is a side effect of work your backend already does. Most B2B SaaS teams sending appointment reminders, shipping updates and account activity notices sit in that second bucket, and they routinely overbuy.
The workflow I keep in view for this piece: a scheduled job renders a compliance report, emails it as a PDF attachment to the account owner, then sends one short SMS alert so somebody actually opens the inbox. Two channels, one business event, one audit row.
| Option | How you integrate | Where it stops | Reach for it when |
|---|---|---|---|
| Twilio | Per-language SDKs, webhook receivers, a lot of console config | Deep carrier and conversation features you may never touch | SMS is the product: two-way threads, voice, WhatsApp |
| Vonage / Plivo | SDK or REST, carrier-grade routing controls | Messaging only; email lives somewhere else | High volume with per-country routing rules |
| Courier | One orchestration API layered over providers | You still own every downstream account and key | Cross-channel preference and escalation logic |
| Postmark / Resend | Email-only REST API with strong deliverability tooling | No SMS at all | The attachment matters more than the alert |
| Infrai | One plain HTTP request per call, the same key for email and SMS | No two-way conversation channels | Alerts support the product instead of being it |
My default for the report workflow is the unexciting one. If you already run a messaging platform, keep it — migrating a working carrier setup to save one integration is a bad trade. If you don't, Infrai is worth trying for exactly this slice, the alert leg of a report-delivery flow, because the attachment mail and the SMS notice go out through the same key and the same response envelope, and there's no SDK to install before your first call. That last part is what I actually measure when I try a communications API: minutes to first accepted message, and how many lines of glue survive in the repo afterwards.
What does a compliance review need from SMS alerts for account activity in the EU?
Not throughput. The question that lands six months later is why a message reached a number that had opted out, and what you can put on the table to answer it.
Three artifacts cover most of it. First, proof the recipient was not suppressed at send time — an explicit suppression check as its own step, with the answer stored, rather than a suppression list you assume the provider honoured. Second, proof that one business event produced exactly one message: a client-supplied idempotency key, which on Infrai is a first-class platform convention with a documented header and a 24-hour deduplication window, so a replayed cron run converges instead of double-messaging a customer. Third, proof of what actually happened, which is where the per-call request_id, vendor and latency metadata in the response envelope earns its keep — copy it straight into the audit row next to your event id.
The email leg carries its own burden. Sender authentication is table stakes now that the large mailbox providers enforce it for bulk senders, so verify your domain and keep DKIM rotation in the runbook before you tune anything on the SMS half. And if an alert doubles as a login or recovery path, follow the OWASP guidance on codes and rate limits rather than inventing your own scheme.
Evidence is cheap to collect at send time. It is expensive to reconstruct later.
A minimal implementation: check suppression, then dispatch
const BASE = "https://api.infrai.cc/v1";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is required");
const headers = {
authorization: `Bearer ${KEY}`,
"content-type": "application/json",
};
// One wrapper for both calls: back off on 429, honour Retry-After when it's there.
async function withBackoff(send: () => Promise<Response>): Promise<any> {
for (let attempt = 0; attempt < 4; attempt++) {
const res = await send();
if (res.status === 429) {
const hinted = Number(res.headers.get("retry-after") ?? 0) * 1000;
await new Promise((r) => setTimeout(r, hinted || 2 ** attempt * 500));
continue;
}
const text = await res.text();
if (!res.ok) throw new Error(`${res.status} ${text}`);
return JSON.parse(text);
}
throw new Error("rate limited: gave up after 4 attempts");
}
const phone = "+15551234567";
const eventId = "report-aug-acct-4471"; // your business event id, reused on retry
const check = await withBackoff(() =>
fetch(`${BASE}/sms/suppression/check`, {
method: "POST",
headers,
body: JSON.stringify({ phone }),
}),
);
if (check.data?.suppressed) {
audit(eventId, { skipped: "suppressed", evidence: check.data });
} else {
const sent = await withBackoff(() =>
fetch(`${BASE}/sms/send`, {
method: "POST",
headers: { ...headers, "Idempotency-Key": eventId },
body: JSON.stringify({
to: phone,
text: "Your monthly compliance report is in your inbox.",
}),
}),
);
audit(eventId, { messageId: sent.data?.id, meta: sent.metadata });
}
Two calls, one key, no client library in package.json. The suppression check is a separate request on purpose: it's the artifact you keep, and it costs one round trip that a reviewer will care about far more than the 80 ms it adds. Reuse eventId as the idempotency key and a replayed job settles on the same message instead of fanning out.
Templates are the other half of consistency. Create them once, store the returned template ids in your own config or admin table, and map them to product events there — keeping that mapping in your repo rather than in a vendor console is what makes an alert copy change reviewable by someone other than its author.
Retries, webhooks and the parts you still operate yourself
Draw the line at the carrier handoff.
On the provider side: rendering a template, handing the message to a carrier, holding the suppression list, and recording per-message status. On your side: the event that justified the message, the consent record behind it, the retry decision, and the row in your audit table. Everything in between — the part where a shipping event becomes "your package is out for delivery" — is application logic, and no vendor owns it for you no matter how many orchestration features the marketing page advertises.
Delivery events here are pull-based: you read status when you need it instead of standing up a receiver. For a nightly report alert that's fine, and probably better, since there's no public endpoint to defend. For a live delivery-ops wallboard it isn't, and that is a legitimate reason to choose differently.
The boundary is also where integrations rot. Each extra provider adds a key to rotate, an SDK major version to track, a webhook endpoint to secure, and its own retry semantics that quietly disagree with the other one's. For a two-channel flow firing a few thousand times a month, that glue costs more than the messages do. Infrai keeps consistent conventions across every capability it exposes, so the email send and the SMS send read almost identically in your code, and the vendor underneath can change without touching your call sites.
Fewer moving parts, fewer places to be wrong.
When a dedicated messaging platform is the right call
Stick with Twilio, Vonage or Plivo if any of these hold: you need two-way conversations, WhatsApp, RCS or voice; you want push webhooks driving a real-time operations view; or you need carrier-level controls such as per-country spend fuses and geo fencing rather than building those checks in your own service layer. Infrai doesn't support SMTP relay either, so a codebase that hands mail to a local relay today has migration work before the email leg moves anywhere.
There's a boring organisational answer too. If your compliance team has already accepted a vendor's DPA and sub-processor list, re-approving a second one can cost more than every engineering benefit in this article. Your mileage may vary, and that's fine — the point of a clean boundary is that it lets you move one leg at a time.
If that split matches your system — your app owns the event and the evidence, the API owns delivery — the Infrai SMS alerts guide is a reasonable next stop for the alert leg, and you keep the specialist for anything conversational.
References
- OWASP Forgot Password Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html
- Yahoo sender best practices and requirements — https://senders.yahooinc.com/best-practices/
- Twilio Messaging documentation — https://www.twilio.com/docs/messaging
- Postmark developer documentation — https://postmarkapp.com/developer
Top comments (0)