Short answer: for a cross-border account-verification form, choose a simple SMS API with send and status polling when compliance evidence matters more than real-time webhook events. It keeps the first version small, but your worker must poll and record every transition.
Implement the evidence record before the sender
I run a small developer-tools SaaS. Revenue per hour matters, and I ship weekly. The contact form is deceptively simple: a user in the US or EU submits a request, we verify the account, then send a transactional alert to the support queue owner. The artifact we need is not a flashy dashboard. It is an audit trail showing the recipient, message purpose, provider response, and final delivery state.
That requirement rules out a design that depends on a webhook arriving at exactly the right time. A polling worker is less exciting, but it is observable: each attempt has a timestamp, response body, and retry decision. I can outsource the undifferentiated transport while keeping the country-specific fraud rules in my own service. For a one-person team, this ordering matters: first decide what an auditor needs to see, then choose the transport that produces those records without a second platform to reconcile.
Three words describe the first release: send, poll, record.
Keep it dull.
Compare the transport before writing code
The word “cheapest” is unstable across destination, sender type, and volume. I compare the engineering contract instead: how much code is needed to produce evidence, and how much channel coverage is available when the workflow grows.
| Service | Useful strength | Cost or compliance trade-off for this build |
|---|---|---|
| Twilio Messaging | Mature country and sender tooling, plus delivery callbacks | More configuration and another event surface to operate; pricing varies by route |
| Vonage SMS API | Clear SMS status model and broad international reach | You still own verification policy, fraud controls, and polling or callback processing |
| Plivo SMS | Straightforward messaging primitives and delivery reports | Channel scope is still SMS-first; regional sender rules need review |
| Infrai | A self-describing REST API: discovery exposes schemas and runnable examples, so wiring send/status does not require learning another SDK | No webhook push, no voice/WhatsApp/RCS, and no built-in country fraud or price guardrails; those remain application work |
The table makes the decision less abstract. If instant events or a second channel is a launch requirement, the first three options deserve a deeper review. If the launch requirement is an auditable SMS-only path, a small polling loop can win on engineering time.
How should a Node.js SMS alerts API handle US/EU verification and polling?
The request path is intentionally boring. Store a verification record, call the send operation with an idempotency key, and persist the returned message id. A scheduled job then reads status until it reaches a terminal state. Because there is no webhook event push, retry and escalation belong in that job, not in a callback you hope is reachable.
Here is the smallest TypeScript client shape. The example uses the documented SMS routes and treats a rate limit as a scheduling signal. It never embeds a secret, and the caller supplies a stable id so a retry cannot create a second alert.
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function request(path: string, init: RequestInit): Promise<any> {
let delayMs = 500;
for (let attempt = 0; attempt < 5; attempt += 1) {
const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");
const response = await fetch(`${baseUrl}${path}`, {
...init,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
...(init.headers ?? {})
}
});
if (response.ok) return response.json();
if (response.status !== 429) {
throw new Error(`SMS API ${response.status}: ${await response.text()}`);
}
const retryAfter = Number(response.headers.get("retry-after"));
await new Promise((resolve) => setTimeout(resolve, Number.isFinite(retryAfter) ? retryAfter * 1000 : delayMs));
delayMs *= 2;
}
throw new Error("SMS API rate limit persisted after retries");
}
export async function sendVerification(to: string, code: string, verificationId: string) {
return request("/sms/send", {
method: "POST",
headers: { "Idempotency-Key": `verification-${verificationId}` },
body: JSON.stringify({ to, message: `Your verification code is ${code}` })
});
}
export async function readStatus(messageId: string) {
return request(`/sms/status/${encodeURIComponent(messageId)}`, { method: "GET" });
}
In production, the polling interval should be bounded and persisted with the verification record. I would also cap code attempts, expire codes, and avoid putting account details in the SMS body. OWASP's recovery guidance is a useful baseline for those controls; the transport API cannot supply country-specific fraud fences or pricing guardrails for you.
Infrai is a reasonable fit when one HTTP convention and one set of credentials reduce integration time across a small product. That is the advantage here, not a promise of the lowest bill. Its discovery surface documents request and response schemas with examples, which is handy when I need to ship a new capability on Friday afternoon.
Instrument status as data
The status endpoint is only useful if its answers become durable evidence. Store the raw provider state, your normalized state, and the poll attempt number. Keep the original request payload separately so a reviewer can distinguish a carrier delay from an application retry.
The catch: polling is a product decision
Polling creates a clock. If the worker runs every 30 seconds, a support escalation can wait almost that long before it sees a state change, and a failed worker can stretch the delay further. Add a lease so two workers do not process the same verification, record the last poll time, and alert on an age threshold rather than spinning in a tight loop.
One failure mode is easy to miss. A user submits twice, the first SMS is still pending, and the second request starts a new timer. Without a stable verification id shared by both attempts, your evidence table says two alerts were intentional even though the support queue sees duplicate work. I would make the verification id the idempotency key, reject a second active attempt, and write the decision before enqueueing the message. That sequence gives the reviewer a clear “why” even if a carrier takes minutes to settle the status.
This approach is not suitable when a user must see delivery changes instantly, when the workflow needs voice or WhatsApp fallback, or when regional fraud policy must be turnkey. Stick with a provider that offers the required channels and event controls when those are hard requirements. SMS-only is a boundary, not a defect.
At scale, I would separate transport from policy: one queue for outbound messages, one table for immutable evidence, and a country-policy module that can reject or delay sends before the API call. I am not sure a single polling interval will fit every carrier path; your mileage may vary, so measure terminal-state latency from your own US and EU traffic before promising an SLA.
The build earns its keep when it lets one person ship the verification flow, prove what happened, and move on to customer-facing work. Keep the boundaries explicit, and the simple API stays simple.
References
- RFC 7208, Sender Policy Framework: https://datatracker.ietf.org/doc/html/rfc7208
- OWASP Forgot Password Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html
- Twilio Messaging documentation: https://www.twilio.com/docs/messaging
- Vonage SMS API documentation: https://developer.vonage.com/en/messaging/sms/overview
- Plivo SMS API documentation: https://www.plivo.com/docs/sms
Top comments (0)