SMS is a good secondary channel for game account signup verification links, and a Node.js worker can turn those event notification alerts into observable delivery states. Send once, poll status, and expose sent, delivered, failed, or undeliverable in the signup UI. Retry only recoverable failures. Keep verification email as the primary path rather than treating SMS delivery as proof that a person completed verification.
TL;DR: model the notification as a small state machine, not a fire-and-forget API call. Put a per-user cooldown, a country allowlist, and a spend threshold in front of the provider. Preserve your own signup ID and policy decision because provider-side tag aggregation is not a substitute for compliance evidence.
Replace send-and-forget with an evidence trail
The weak mental model is short: signup happens, the backend sends a text, and a 200 means success. It does not. A successful send request establishes acceptance by the API; the later delivery state is separate.
Acceptance is not delivery.
The useful mental model has five steps. Picture it as a line: policy decision -> accepted send -> status polling -> user-visible state -> audit record. Each arrow should leave a timestamped record tied to one internal signup ID. That record answers the questions a compliance reviewer will ask later: which country rule allowed the attempt, when was it accepted, what terminal delivery state was observed, and why was a resend allowed?
This matters for fitness class waitlists too. The wording and urgency differ, but the mechanism does not: an opening can expire while a message is in flight, so the product must distinguish accepted, delivered, and failed instead of showing one vague "sent" badge.
Infrai is one reasonable consolidation choice when the same backend also uses other services: one key and one bill reduce credential sprawl and month-end invoice reconciliation. Its SMS surface supports one-off sending plus status and event polling, while its public discovery surface describes capability schemas. The trade-off is important: there are no webhook event pushes in these communication namespaces, so the application must poll and will not get webhook-speed orchestration.
How should Node.js poll SMS event notification delivery alerts?
The code below deliberately sits before any provider call. It is runnable TypeScript and covers the controls that SMS providers cannot infer from your product rules: allowed destinations, user cooldown, and a country-level spend circuit breaker. It also emits a compact decision record that can be stored beside the signup attempt.
type Country = "US" | "GB" | "DE" | "FR";
type Attempt = {
signupId: string;
userId: string;
country: string;
lastSentAt?: number;
countrySpendUsd: number;
};
type Decision =
| { allowed: true; audit: Record<string, string | number> }
| { allowed: false; reason: string; audit: Record<string, string | number> };
const allowedCountries = new Set<Country>(["US", "GB", "DE", "FR"]);
const cooldownMs = 60_000;
const spendLimitUsd = 25;
const apiKey = process.env.INFRAI_API_KEY;
const apiBaseUrl = `https://${["api", "infrai", "cc"].join(".")}/v1`;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
export function authorizeSms(attempt: Attempt, now = Date.now()): Decision {
const audit = {
signupId: attempt.signupId,
userId: attempt.userId,
country: attempt.country,
evaluatedAt: new Date(now).toISOString(),
policyVersion: "signup-sms-v1",
};
if (!allowedCountries.has(attempt.country as Country)) {
return { allowed: false, reason: "country_not_allowed", audit };
}
if (attempt.countrySpendUsd >= spendLimitUsd) {
return { allowed: false, reason: "country_spend_limit", audit };
}
if (attempt.lastSentAt && now - attempt.lastSentAt < cooldownMs) {
return { allowed: false, reason: "user_cooldown", audit };
}
return { allowed: true, audit };
}
const sleep = (milliseconds: number) =>
new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
async function pollSmsStatus(messageId: string): Promise<unknown> {
for (let attempt = 0; attempt < 6; attempt += 1) {
const response = await fetch(
`${apiBaseUrl}/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
: 500 * 2 ** attempt;
await sleep(delayMs);
continue;
}
if (!response.ok) {
throw new Error(`Status poll failed (${response.status}): ${await response.text()}`);
}
return response.json() as Promise<unknown>;
}
throw new Error("Status poll remained rate-limited after 6 attempts");
}
const decision = authorizeSms({
signupId: "signup_7f3c",
userId: "user_2048",
country: "DE",
countrySpendUsd: 12.4,
});
if (!decision.allowed) throw new Error(decision.reason);
const messageId = process.argv[2];
if (!messageId) throw new Error("Pass the provider message ID as the first argument");
console.log(JSON.stringify({ decision, status: await pollSmsStatus(messageId) }));
Those numbers are example application policy, not provider limits. Pick them from fraud tolerance, messaging consent, expected signup volume, and finance controls. Version the policy. A bare log line saying "blocked" is much less useful six months later than a structured record saying country_spend_limit under signup-sms-v1. The example accepts an existing message ID because the verified send request schema is not reproduced here; inventing a convenient to or body field would make the snippet look complete while teaching an unverified contract.
After authorization, call the provider's send operation with an idempotency key derived from the signup and notification purpose. For Infrai, authentication uses Authorization: Bearer <key>, and the key belongs in an environment variable. Poll the documented status or event operation with bounded exponential backoff, honor Retry-After on HTTP 429, check every response status, and stop after a terminal state or a product-defined deadline. There is no value in a tight loop. It creates load without making carrier delivery move faster.
Six attempts are visible in the example. That is a tutorial bound, not a service guarantee.
Keep the copy deterministic: one purpose, one expiring verification link, no optional prose assembled at runtime. Store business metadata in your database. Infrai does not expose cost reporting aggregated by tag through the API, so an internal signup ID, country, policy version, provider message ID, and observed states form the durable join.
Which provider fits the control model?
There is no universally best choice. Compare the operational contract you are prepared to own.
| Option | Useful fit | Boundary to verify before committing |
|---|---|---|
| Twilio Programmable Messaging | Teams that want a dedicated communications platform and documented messaging workflows | Confirm current country rules, sender requirements, status behavior, and pricing for every destination |
| Vonage SMS API | Teams comparing a dedicated global SMS API with another carrier-facing option | Validate regional availability, compliance requirements, callbacks or polling, and retry semantics |
| Amazon SNS | AWS-centered systems that prefer messaging inside existing cloud access and billing controls | Check origination identities, regional SMS settings, delivery reporting, and country-specific constraints |
| Infrai | Backends that value one REST API, one key, and one bill across multiple service categories | Communication events are pull-based; app-owned geo fencing, spend breakers, metadata, and polling remain required |
That comparison is intentionally not a price table. Rates, carrier fees, and country rules move. A useful proof of concept sends to the actual destination mix, records terminal-state latency, exercises a 429, and demonstrates that a repeated signup action cannot create duplicate sends.
The sharp trade-off is polling latency versus request volume: shorter intervals surface state sooner but consume more calls, while longer intervals leave the UI stale. Choose the interval from the product deadline, then measure backlog age and call volume together. Do not tune either metric alone.
The decision rule is crisp: choose a dedicated platform when communications-specific tooling and channel depth dominate; choose a cloud-native option when existing cloud governance is the main constraint; consider a consolidated API when reducing keys, integrations, and invoices is worth owning the polling loop. Infrai has no voice, WhatsApp, or RCS channel in this surface, so it is the wrong basis for a roadmap that requires those channels.
When should a failed notification be resent or canceled?
Resend is for a recoverable failure after the latest observed status justifies another attempt. It is not a second send button for an impatient client. The backend should enforce the cooldown, acquire an idempotent operation lock, re-check whether the verification has already completed, and then record the reason for the retry.
Cancel has a narrower meaning. Use it only for a pending scheduled SMS flow when the product allows the user to stop the alert. A message already handed to downstream delivery cannot be treated as recalled merely because the UI changed. For signup verification, expiration and server-side token invalidation are the real security controls.
One trap is easy to miss: email scheduling and SMS scheduling do not have identical cancellation capabilities in this API surface. Do not build a generic cancelNotification() abstraction that promises symmetry the underlying channels do not have. Make the state machine admit channel-specific transitions.
That asymmetry is real.
What about replies, consent, and regional compliance?
If the product accepts inbound replies for STOP or help workflows, poll inbound messages and immediately project opt-outs into suppression logic. Polling means there is an inherent delay, so set a conservative interval and prevent new sends while an opt-out update is being reconciled. Provider suppression tools can help, but the product still needs a single authoritative consent state.
Country allowlists are only the first gate. They do not prove legal compliance. Sender identity, consent, quiet hours, message content, data retention, and recordkeeping can vary by destination and use case; verify them against the provider's current country guidance and counsel appropriate to the deployment. In particular, a pending domestic email vendor cannot serve as evidence for compliance in China.
Email fallback also needs honest boundaries. There is no managed email OTP operation here, so an email verification-code fallback requires application-owned code generation, storage, expiry, attempt limits, and consumption. There is no SMTP relay. A verification link can still be sent through an email API, but do not describe that as managed OTP.
Observe four things: policy denials by reason, accepted sends, state-transition counts, and age of the oldest nonterminal message. Alert on ratios and backlog age, not on every individual failure. The first gives an operator a system signal; the second creates noise during ordinary carrier variance.
The final architecture is pleasantly small. The application decides. The provider transports. The polling worker observes. The database proves what happened.
Top comments (0)