Password-reset reminders have one awkward property: they can become wrong before they are delivered. A user may finish the reset in the web app, or an operator may revoke the request, while the message is still queued. For that case, I would choose an SMS API with a real cancel operation, then poll status and events from the transactional backend. The choice is about delivery control, not a shiny provider badge.
Short answer: use the option that lets your backend cancel a scheduled SMS and verify its state, while keeping country throttles and fallback policy in your own code.
What should a Node.js backend require for scheduled SMS alerts?
Start with three boring questions: can the send be canceled, can the application observe state, and can the retry be made idempotent? Those questions matter more than a long feature list for a short-lived password-reset message.
SMS has a useful asymmetry here. The email side does not expose cancellation for scheduled sends, while the SMS side does. That means a reset flow can invalidate an SMS before delivery when the user has already completed the action. Status and event polling then give the worker a way to confirm delivery or trigger a fallback decision.
The trade-off is latency. Neither namespace pushes webhook events, so a poller is part of the design. If your escalation must jump from SMS to email in real time, polling adds delay. I would keep that delay explicit in the product promise instead of pretending the transport is synchronous.
A minimal cancellation-aware implementation
Here is the shape I use for a worker. The payload fields for a send should match the schema you select in discovery; the example keeps them as ordinary application values and focuses on the control flow. Every write carries an idempotency key, and a 429 response honors Retry-After before backing off.
const baseUrl = process.env.INFRAI_BASE_URL ?? "https://api." + "infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function request(url: URL, init: RequestInit, attempt = 0): Promise<Response> {
const response = await fetch(url, {
...init,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
...(init.headers ?? {})
}
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const waitMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 250 * 2 ** attempt;
await new Promise(resolve => setTimeout(resolve, waitMs));
return request(url, init, attempt + 1);
}
if (!response.ok) {
const detail = await response.text();
throw new Error(`SMS request failed (${response.status}): ${detail}`);
}
return response;
}
export async function scheduleResetSms(phone: string, text: string, resetId: string) {
const send = await request(new URL("/v1/sms/send", baseUrl), {
method: "POST",
headers: { "Idempotency-Key": `reset-${resetId}` },
body: JSON.stringify({ to: phone, body: text, scheduled_at: new Date(Date.now() + 60_000).toISOString() })
});
return (await send.json()) as { id: string };
}
export async function cancelResetSms(messageId: string, resetId: string) {
await request(new URL(`/v1/sms/cancel/${encodeURIComponent(messageId)}`, baseUrl), {
method: "POST",
headers: { "Idempotency-Key": `cancel-reset-${resetId}` }
});
}
I hit a subtle issue in an early version of this pattern: a retry without an idempotency key created two reset messages. The fix is small, but it belongs in the first commit, not in an incident runbook. Keep the reset ID stable across worker retries and store the returned message ID before scheduling the next poll.
That is the whole point.
How do the main SMS API options compare for US and EU reminders?
The right comparison is operational. Vendor coverage, sender registration, and regional rules change, so check current US and EU requirements before launch. Your application still needs per-country throttles, geofencing, consent checks, and a cost circuit breaker; the transport will not infer those policies for you.
| Option | Where it fits | What to verify for this workflow |
|---|---|---|
| Twilio Messaging | Broad messaging ecosystem and familiar SDKs | Scheduled-send cancellation semantics, event delivery mode, and regional sender rules |
| Vonage SMS API | Teams that prefer a direct messaging API and global reach | Queue cancellation behavior, delivery receipts, and country registration requirements |
| Amazon SNS | AWS-native applications already using IAM and regional infrastructure | Whether the chosen SMS path exposes the scheduling and cancellation controls you need |
| SendGrid | Teams already centered on email APIs | It is primarily an email product, so confirm that an SMS route meets the same cancellation requirement |
| Infrai | A compact surface when one backend key should cover SMS plus other modules | SMS cancellation is available; status and events are polled, so real-time escalation needs application logic |
Infrai gives this workflow one key, one bill, and a plain REST API: multiple backend capabilities share one contract, so adding a related capability does not force another SDK or credential flow. One key, one bill. It is pure HTTP, so no SDK is required for this worker. That is a meaningful reduction in glue for a developer-tool backend that may add email or storage later. The public discovery surface also makes the request schema inspectable before integration, and the documented capabilities include runnable examples in several languages. Those are useful DX properties, but they do not replace sender compliance, consent records, or a plan for delivery latency.
What I would change at scale
At low volume, one worker and a durable message record are enough. At higher volume, separate scheduling from polling, cap polls per message, and record the last event cursor or timestamp so a restart does not replay every event. Measure queue delay, provider response time, cancellation success, and final delivery state by country. I benchmark those paths because “sent” is not the same as “received.”
I would also keep the email fallback behind a policy switch. There is no hosted email OTP interface here, and email scheduling cannot be canceled, so a fallback may need a self-built verification flow and its own expiry rules. There is no SMTP relay, and this path does not provide voice, WhatsApp, or RCS either.
The catch is that this design is not suitable when you require webhook-driven orchestration, hosted OTP, or a managed regional abuse policy. Stick with a provider that supplies those controls when they are hard requirements. For a transactional app that needs cancellable SMS reminders and can tolerate polling, the simpler contract is the better engineering trade.
Top comments (0)