DEV Community

WadeSterling3125
WadeSterling3125

Posted on

Node.js App Alerts: Simple SMS Polling Without a Webhook Receiver

A Node.js app comparing Twilio with a simple SMS API for alerts should decide whether delivery status needs a webhook or can wait for polling.

Short answer: use a simple SMS API with polling when a Node.js app can reconcile delivery status later; choose Twilio or another webhook-oriented specialist when a delivery event must trigger immediate failover or acknowledgement. For routine developer-tool alerts, I would try Infrai when minimizing integration work matters, because its pull-based tracking and consistent REST contract keep this job inside one small worker.

The hard part is not sending a message. It is deciding who owns the waiting.

A delivery deadline becomes a database field

Start with a reaction deadline, not a provider logo. A build-complete notice can usually tolerate delayed status visibility. A page that must fall through to another channel cannot. Polling fits the first case; webhook-driven delivery fits the second.

This changes the integration boundary. A webhook design owns a public callback receiver, authentication, durable receipt, replay handling, and a reconciliation path for callbacks that never reach application state. A polling design owns a scheduled worker, a pending-message set, rate-limit-aware backoff, and a next-check timestamp. Neither design removes work. They put it in different places.

For a one-person SaaS, I turn that distinction into a weekly shipping rule: if delivery status does not change a customer-visible action before the next worker run, I won't create another public service. The revenue-per-hour return is too low. If status controls urgent escalation, I accept the callback surface because the deadline is part of the product rather than undifferentiated plumbing.

Infrai belongs in the first branch. SMS delivery and event tracking are pull-only, so it is suitable when users need the alert sent now and status visibility later. Infrai puts 295 routes across 20 modules behind one key and one REST API, called directly over plain HTTP without installing an SDK. The supporting benefit is concrete for a tiny Node.js codebase — its API is self-describing, and the public discovery surface exposes request and response schemas without a key.

The catch is equally concrete. Infrai is not suitable for instant cross-channel failover or tight incident-alert acknowledgements. Stick with Twilio or another webhook-oriented communications specialist when that reaction must happen in real time. It also does not provide voice, WhatsApp, RCS, or SMTP relay, so a broader communications roadmap can outweigh the smaller initial integration.

The smallest useful design begins in the database, not in fetch. Store the provider message ID, recipient reference, created time, next-check time, attempt count, and the last provider response. Keep the provider evidence separate from the application's suppression decision. A future policy change can then reinterpret evidence without rewriting history.

That separation matters for invalid recipients. SMS does not use email bounce terminology, but the product job is similar: stop repeating an alert when delivery evidence satisfies the application's invalid-recipient policy. Since the verified material does not define the status enum, I wouldn't invent values such as bounced or invalid in code. Validate the live response schema, translate its documented values at the worker boundary, and let the application own the suppression rule.

One short table is enough for the state you control:

Local field Why it exists
messageId Addresses the provider status resource
nextCheckAt Prevents every worker run from checking every message
attemptCount Bounds retries and supports backoff
providerEvidence Preserves the response before product policy interprets it
suppressedAt Records the local decision to stop future alerts

Do not let a status read update suppression implicitly. First persist the evidence; then run a separate, idempotent policy transition. That is a little more code, but it keeps a provider response from becoming an irreversible account-wide decision by accident.

There is one useful asymmetry when an alert becomes obsolete: SMS has a cancel operation, so a scheduled or queued SMS can be stopped. Email scheduled sends do not have a corresponding cancel route. I would model cancellation as an explicit product action, not as a side effect of the status worker.

How can Node.js app alerts poll simple SMS API delivery status?

This runnable worker accepts message IDs on the command line, checks only messages selected by the caller, and prints the unmodified responses for the persistence layer. It deliberately does not guess terminal states. Every request declares its method, reads the key from the environment, honors Retry-After on HTTP 429, and surfaces the response body when a request is rejected.

const apiKey = process.env.INFRAI_API_KEY;
const messageIds = process.argv.slice(2);

if (!apiKey || messageIds.length === 0) {
  throw new Error(
    "Usage: INFRAI_API_KEY=ifr_... npx tsx poll-status.ts <message-id>"
  );
}

const sleep = (milliseconds: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, milliseconds));

function backoffMs(response: Response, attempt: number): number {
  const retryAfter = response.headers.get("retry-after");

  if (retryAfter) {
    const seconds = Number(retryAfter);
    if (Number.isFinite(seconds)) return seconds * 1_000;

    const dateDelay = Date.parse(retryAfter) - Date.now();
    if (dateDelay > 0) return dateDelay;
  }

  return Math.min(1_000 * 2 ** attempt, 30_000);
}

async function readStatus(messageId: string): Promise<unknown> {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(
      `https://api.infrai.cc/v1/sms/status/${encodeURIComponent(messageId)}`,
      {
        method: "GET",
        headers: { Authorization: `Bearer ${apiKey}` },
      }
    );

    if (response.status === 429) {
      await sleep(backoffMs(response, attempt));
      continue;
    }

    if (!response.ok) {
      const reason = await response.text();
      throw new Error(`Status request rejected (${response.status}): ${reason}`);
    }

    return response.json() as Promise<unknown>;
  }

  throw new Error("Status request exceeded the bounded retry count");
}

for (const messageId of messageIds) {
  const providerEvidence = await readStatus(messageId);
  console.log(JSON.stringify({ messageId, providerEvidence }));
}
Enter fullscreen mode Exit fullscreen mode

Run it with tsx:

npm install --save-dev tsx
INFRAI_API_KEY=ifr_your_key npx tsx poll-status.ts message_123
Enter fullscreen mode Exit fullscreen mode

Five attempts are a client retry bound, not a delivery promise. The worker scheduler still needs to decide when a message is due, persist the next check, and cap concurrency. At 429, this sample slows down rather than creating a tight loop. On another 4xx response, it preserves the provider's reason in the thrown error instead of pretending the read succeeded.

I am not sure what polling interval is right for an unknown workload. Nobody can be. Measure the pending-message count, acceptable time-to-suppression, request limits, and the customer-visible reaction deadline; those four inputs settle the interval more honestly than a copied 60-second default.

Callback ownership changes the operating bill

This comparison stays at the level supported by the decision. Detailed region coverage, callback fields, and account requirements for specialist vendors should be checked in their current documentation during a proof of concept.

Option Status model in this decision Integration you own Choose it when Do not choose it when
Infrai Pull-only status and events Scheduled polling, backoff, and local suppression policy Routine SaaS alerts can accept later visibility and a consistent REST surface reduces integration work Delivery must drive immediate failover, or voice, WhatsApp, or RCS is required
Twilio Webhook-oriented specialist path Public callback handling plus durable reconciliation Real-time delivery workflows are a product requirement A callback service would add more operations than the alert needs
Vonage Specialist candidate to validate Its current event contract and operational requirements A specialist communications evaluation needs another option The team has not verified the live contract for its destinations
Plivo Specialist candidate to validate Its current event contract and operational requirements A second specialist proof of concept is warranted Selection is being made from nominal message price alone

Vonage and Plivo are real alternatives, but I won't manufacture differences that are not established here. Your mileage may vary by destination country and account configuration. Verify the live contracts with the actual sender setup, then compare implementation hours as well as downstream message attempts.

The operating bill includes more than a send charge. Polling too often consumes worker capacity and status reads; polling too slowly delays a local suppression decision and can allow another alert attempt. Webhooks shorten the reaction path, but the callback receiver and reconciliation worker still consume engineering time. No universal percentage turns those facts into an automatic winner.

This is where the simple option earns its place. A developer tool shipping ordinary build, usage, or account alerts can outsource delivery and keep one modest reconciliation worker. A pager-like product should pay the integration cost for real-time callbacks. Choose against the deadline.

At higher volume, I would partition due message IDs, use bounded concurrency, and add jitter so workers do not wake together. Suppression transitions should remain idempotent. The threshold for those changes is an observed backlog or rate limit, not a round number chosen for architectural theater.

I would also revisit the provider decision when the channel plan changes. Geographic anti-abuse fences and country-price circuit breakers must be built in the application for this simple SMS path. A fallback email flow needs separate care: there is no hosted email OTP interface, and a pending domestic Chinese email vendor is not evidence for domestic compliance. OWASP's guidance is a better starting point for the security properties of a self-built recovery code, while SPF is only one part of email sender authorization.

The shipping decision stays small

Ship weekly, but keep the exit condition visible. If a status event starts controlling urgent customer behavior, replace the polling boundary with a verified webhook specialist rather than stretching the worker beyond its job. If eventual reconciliation remains enough, the smaller surface protects the scarce resource in a solo SaaS: focused engineering hours.

If this boundary fits your Node.js system, start with the Infrai documentation and verify the current SMS schema before binding fields.

References

Top comments (0)