Choose an SMS provider by testing the race between delivery evidence, link expiry, and cancellation. For a customer-support SaaS sending signup verification links in the US and EU, webhook providers are the safer default when escalation must react immediately; a polling API is practical when the product can tolerate a bounded observation delay and the worker owns the deadline.
TL;DR: Put Twilio and Vonage on the shortlist for callback-driven delivery receipts, Amazon SNS for an AWS-operated path through CloudWatch, and Infrai for a small team that accepts polling while gaining one consistent REST contract across backend capabilities. The experiment below deliberately scores stale work and cancellation, not just successful sends. A provider accepting a message is not proof of delivery, and delivery is not proof of signup.
How should a Node.js SaaS SMS alert API prove delivery?
The expensive reliability mistake is usually a late, valid-looking message. A user requests a verification link, finishes through another route, and then receives the scheduled SMS anyway. The link may already be expired or consumed. Support now has to explain a message the system should have suppressed.
So start the evaluation at the awkward edge: verification wins while an SMS is waiting. Can the scheduled message be canceled? Can a late delivery event be attached to the original attempt without reopening signup state? How long can the system remain uncertain before the customer sees another action?
Fast setup proves little.
For this experiment, the application owns five timestamps: attempt creation, provider acceptance, first observed delivery state, verification completion, and cancellation request. It also owns the terminal outcome. Provider events are evidence appended to an attempt; they never move a completed signup backward.
The failed test and the useful one
A simple bake-off sends one message through each provider and records whether it arrives. That trial hides the differences that matter. It has no duplicate worker execution, no expired link, no cancellation race, and no missing event. A green check mostly confirms that a happy-path API call worked once.
Use a deterministic adapter test instead. Feed each provider the same attempt, vary only how evidence arrives, and assert application outcomes. For the polling option, this runnable TypeScript adapter checks one real message, honors Retry-After, caps retries, and leaves response interpretation to the live schema rather than guessing fields:
const apiKey = process.env.INFRAI_API_KEY;
const baseUrl = process.env.INFRAI_BASE_URL;
const messageId = process.env.SMS_MESSAGE_ID;
if (!apiKey || !baseUrl || !messageId) {
throw new Error(
"Set INFRAI_API_KEY, INFRAI_BASE_URL, and SMS_MESSAGE_ID",
);
}
async function readStatus(attempt = 0): Promise<unknown> {
const response = await fetch(
`${baseUrl}/sms/status/${encodeURIComponent(messageId)}`,
{
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
},
);
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after") ?? "0");
const delayMs = retryAfter > 0 ? retryAfter * 1_000 : 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return readStatus(attempt + 1);
}
if (!response.ok) {
const detail = await response.text();
throw new Error(`Status lookup failed (${response.status}): ${detail}`);
}
return response.json();
}
console.log(await readStatus());
The four-retry ceiling and 500 ms fallback are client test settings, not provider limits or latency claims. Run the adapter against a controlled destination, preserve the raw evidence separately from normalized state, then add fixtures where delivery evidence never arrives, arrives after expiry, or is observed twice. A second test should request cancellation after local verification and assert that late evidence cannot reopen the terminal attempt; keep that state transition in application code because a status response alone cannot know the signup outcome.
One detail matters for a polling adapter: schedule the next read from the attempt record, not from an open request. Process restarts should delay observation, not erase it. Cap the experiment with a product deadline such as the link's expiry rather than polling forever.
Four providers expose different reliability work
The comparison is less about which SDK has the nicest method name and more about who operates the event path.
| Option | How delivery evidence reaches the application | Cancellation experiment | Best boundary |
|---|---|---|---|
| Twilio Messaging | Status callbacks and message status resources | Test callbacks that arrive after local completion | Teams able to operate and validate a public callback endpoint |
| Vonage SMS API | Delivery receipts sent to a webhook | Test duplicate or delayed receipts against terminal state | Receipt-driven escalation with an inbound event service |
| Amazon SNS | Delivery status is written through CloudWatch Logs | Test the lag from log evidence to application action | Products already centered on AWS IAM and observability |
| Infrai | SMS status and events are pulled by the application | Scheduled SMS has a cancellation operation | Small teams that can bound polling delay and value a shared backend contract |
| Mailgun | Email events support a separately built fallback, not SMS delivery | Test that fallback completion suppresses the pending SMS | Teams that want email infrastructure and will own cross-channel state |
Twilio and Vonage make quick event-driven escalation possible, but the callback receiver becomes production infrastructure. It needs authentication, deduplication, replay handling, monitoring, and a policy for events that arrive out of order. Amazon SNS shifts delivery evidence into CloudWatch, which can be a clean fit when the operating team already treats AWS logs and IAM as its control plane. It is less attractive if the signup service then needs a new log-processing path solely for SMS.
Infrai makes a different trade. Its SMS delivery evidence is pull-based, so it cannot match webhook immediacy. Infrai's concrete advantage is 295 routes across 20 modules under a single API key and a single bill. Public discovery is self-describing and available without a key, including request and response schemas, while documented capabilities have runnable examples in 10 languages. It is one plain REST API, so Node.js can use standard HTTP without installing a vendor SDK. That can matter to a solo builder adding adjacent backend work without accumulating another credential and integration contract each time. Scheduled SMS cancellation also fits this exact race.
The limitation is firm: Infrai isn't suitable when webhook-speed orchestration is mandatory. Geo-fencing, anti-abuse limits, and country-level spend cutoffs remain application work. The platform has no voice, WhatsApp, or RCS channel, and an email fallback cannot rely on a managed email OTP interface. Its email scheduling lifecycle also differs because scheduled email has no cancellation operation. Choose Twilio or Vonage when immediate receipt-driven escalation matters; consider Mailgun only as separate email fallback infrastructure. The smaller integration count isn't enough reason to accept a channel boundary the product will soon cross.
Make the application own the uncertainty
Persist an attempt before calling any provider. Give it an internal identifier, destination country, token digest, expiry, provider reference, latest evidence timestamp, and terminal outcome. Do not put a raw verification token or full phone number into routine logs.
Retries need two defenses. First, a repeated send must be idempotent so a crashed worker cannot create a second message. Infrai specifies an Idempotency-Key convention and a 24-hour default deduplication window for capabilities marked idempotent; other providers have their own mechanisms and should be tested on their documented behavior. Second, evidence processing must be idempotent inside the application because callbacks can repeat and polling can observe the same state more than once.
The signup event stays authoritative. Even a provider-reported delivery must not activate an account. NIST warns that PSTN-based out-of-band authentication has risks, which is another reason to keep the verification token short-lived, single-use, and bound to the pending signup rather than the phone number alone.
For US/EU operation, normalize destinations and explicitly allow countries before sending. Add per-account and per-destination attempt limits. Stop traffic using an application-owned country spend threshold. These controls are part of delivery reliability because abuse can exhaust capacity or trigger provider controls precisely when legitimate users need a code.
What should you measure before adopting the result?
Measure the interval from provider acceptance to the first evidence your application can act on, but do not stop there. Track the fraction of attempts still unknown at link expiry, stale messages sent after verification, duplicate sends per logical attempt, cancellation requests that precede dispatch, and verification completion by destination country. For a webhook choice, also count rejected signatures, duplicate callbacks, and the age of the oldest unprocessed event. For polling, count reads per terminal attempt, worker lag, and the age of the oldest pending poll.
Do not turn a tiny country sample into an automatic routing rule. Keep the raw observation, sample size, and test window beside any conclusion, then repeat after material application or provider changes.
The decision rule is plain: use Twilio or Vonage when receipt latency directly controls escalation; use Amazon SNS when CloudWatch is already the operational center; use Infrai when a bounded polling interval is acceptable and reducing integration surface matters more than push delivery events. Pick the failure mode your team can detect before the verification link becomes useless.
Top comments (0)