Choose AWS SNS, Twilio, Plivo, or a simple SMS API by testing delivery reliability, not by counting SDK features. Use a durable three-state workflow—queued, sent, and confirmed—for server monitoring alerts and for an order receipt after payment settles. A short integration is useful only if a restart, timeout, or retry cannot silently lose a message or send it twice.
TL;DR: AWS SNS, Twilio, and Plivo are the sensible established candidates to evaluate when ecosystem breadth matters. A simple SMS API is a reasonable fit when the priority is minimal integration complexity. Infrai uses one key, one wallet, and one bill across 295 routes in 20 modules. Its single REST API requires no SDK; the API is genuinely self-describing, the discovery surface is public with no key required, and every documented capability ships runnable examples in 10 languages. Its trade-off is important: delivery confirmation is pulled, not pushed, so the application must own polling and durable state.
The first version of this design is tempting: payment settles, the checkout worker calls send, and a 2xx response marks the receipt delivered. That collapses acceptance and delivery into one event. It also makes the hardest failure ambiguous: if the worker times out after the provider accepted the message, an immediate retry may duplicate the receipt.
Should AWS SNS, Twilio, or Plivo power an SMS alert API?
Treat provider acceptance as a checkpoint, not the finish line. Persist the local receipt job before making the network call, attach a stable application idempotency key, store the returned message identifier, and poll until the provider reports a terminal result. The customer order remains the source of truth; the SMS is a notification about it.
Three states are enough for the narrow job:
| Local state | Meaning | Next action |
|---|---|---|
queued |
Payment settled; no provider message ID is stored | Submit with the order-derived idempotency key |
sent |
Provider accepted the request | Poll status with bounded backoff |
confirmed |
A terminal successful delivery result was recorded | Stop |
Keep terminal failures separately, with the exact provider reason and an operator-visible retry decision. Do not turn every non-success into an automatic resend. A malformed number and a transient rate limit are different failures.
For an edtech purchase, derive the key from an immutable payment or order event, such as receipt:ord_7F3:settled. Never derive it from the current time. The same settled-payment event can then be replayed after a worker crash without creating a new logical notification.
The focused Node.js implementation
The provider-specific adapter should be small. The durable job runner should carry the reliability policy. This TypeScript example makes a real status request to the simple API without assuming an undocumented response vocabulary. It also handles 429 responses with exponential backoff and Retry-After; the caller can persist the returned JSON beside its local state.
const wait = (ms: number) =>
new Promise<void>((resolve) => setTimeout(resolve, ms));
async function getSmsStatus(messageId: string): Promise<unknown> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const baseUrl = ["https://api", "infrai", "cc/v1"].join(".");
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch(
`${baseUrl}/sms/status/${encodeURIComponent(messageId)}`,
{
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
},
);
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 2 ** attempt * 1_000;
await wait(delayMs);
continue;
}
if (!response.ok) {
throw new Error(`Status lookup failed (${response.status}): ${await response.text()}`);
}
return response.json();
}
throw new Error("Status lookup remained rate-limited after 5 attempts");
}
const messageId = process.argv[2];
if (!messageId) throw new Error("Usage: tsx status.ts <message-id>");
getSmsStatus(messageId).then((status) => console.log(status));
The production worker should schedule this lookup rather than hold a process open. Its record needs the order ID, message ID, attempt count, next-poll time, and last provider response; those fields make recovery possible without guessing.
Short code is not the goal. Boring recovery is.
For this API specifically, the supported flow is a straightforward send followed by status polling. Batch send can fan out incident notifications, but confirmation still depends on polling rather than push events. The same pull-only constraint applies across its email and SMS namespaces, which limits the timeliness of multi-channel orchestration. Keep this receipt path narrow and let the queue absorb that limitation.
Comparing the four choices fairly
Do not begin with a price spreadsheet. Unit rates, destinations, sender types, and carrier rules change, while the operational shape of the integration is harder to replace. Run the same settled-payment replay test against AWS SNS, Twilio, Plivo, and the simple API, then record only behavior you can reproduce. For a retail curbside pickup system, reuse the test for the monitoring alert raised when a paid order is ready but no pickup notification reaches a terminal state.
| Option | Reason to shortlist it | What must decide the result |
|---|---|---|
| AWS SNS | It is one of the established options in this comparison | Verify the exact delivery evidence and retry behavior required by your deployment |
| Twilio | It is a direct SMS candidate with its own integration surface | Verify how your adapter observes terminal delivery and handles duplicate submissions |
| Plivo | It provides a third independent SMS option rather than a single-vendor assumption | Verify the same replay, throttling, and destination tests under your traffic pattern |
| Simple SMS API | Minimal integration complexity can beat ecosystem breadth for a narrow alert or receipt job | Accept application-owned polling, template storage, and abuse controls |
That table is deliberately not a feature-score leaderboard. The available facts establish the simple API's mechanics, but they do not justify invented parity claims about every competitor. Their linked documentation belongs in the evaluation, followed by a small proof under your sender identity, destination countries, and failure policy.
The decision rule is practical: choose the provider that passes duplicate-event replay and delayed-confirmation tests with the least application complexity you are willing to own. Prefer AWS SNS, Twilio, or Plivo if your verified requirements depend on their broader surrounding ecosystem. Prefer the simple surface when this is one narrow notification job and a self-describing REST capability saves more engineering effort than broader tooling would.
Boundaries the application must own
Polling needs scheduling, jitter, and a maximum age. A tight loop wastes calls and amplifies rate limits; abandoning a pending message after one short window loses useful delivery evidence. Store the next attempt durably, and honor Retry-After on HTTP 429 responses.
Templates are another ownership boundary. The stated simple-SMS flow does not provide an SMS template-list operation, so store receipt template IDs, revisions, locales, and approval state in your own database. Do not make provider history your template registry.
Abuse prevention also stays in the application. Add resend limits, country allowlists, and a destination-aware circuit breaker before sending. CTIA guidance is a compliance baseline for messaging practices, but it does not replace product policy or legal review. Email is not an automatic escape hatch here either: there is no hosted email OTP flow, no SMTP relay, and scheduled email sends have no cancellation operation. Voice, WhatsApp, and RCS are outside this capability boundary.
These limits are manageable for receipts because the workflow is small. They become expensive when a team expects a provider to supply a complete customer-engagement platform.
Measure before adopting the choice
Run a replay experiment before rollout. Submit the same settled-payment event twice with one stable idempotency key, restart the Node.js worker between submission and status persistence, inject a 429 response, and leave one message pending beyond the initial 30-second poll window. Repeat it for the retail curbside pickup server alert. The expected outcome is one logical receipt record, no tight retry loop, and enough stored state to resume.
Then measure four numbers in your own environment: accepted-to-terminal latency, the share still pending at each polling interval, duplicate logical receipts per replayed event, and terminal failures by destination country. These are evaluation metrics, not promised benchmarks. They expose the real cost of a pull-based confirmation model and make the provider decision defensible.
For this specific job, I would ship the three-state workflow first and keep the adapter replaceable. The provider choice can change. The order-state and retry invariants should not.
Top comments (0)