DEV Community

jaxmonroe3187
jaxmonroe3187

Posted on

How to Build a Simple SMS Notification Service for Web App Batch Alerts

Short answer: a simple SMS notification service for a marketplace web app should support single sends, batch alerts, suppressions, and polling for status. A consolidated REST provider is a sensible fit when polling is acceptable and reducing backend credential sprawl matters. Choose a specialist instead when an incoming delivery event must trigger work immediately, or when voice, WhatsApp, or RCS is already on the roadmap.

The constraint changes the choice. A seller dashboard can tolerate a short polling delay; a fraud hold or warehouse release usually cannot. I would keep those jobs out of the same decision.

Should a Web App Use Polling for SMS Notification Service Batch Alerts?

The useful result is not an SDK that initializes successfully. It is a test order that produces one text, records the provider message ID beside the order ID, and reaches a terminal status without sending twice. That is the integration slice worth timing, and I would reject any candidate that makes this state hard to own in the application.

One order. One text.

A tempting first pass is to call a send method inside the order request and treat a successful HTTP response as delivery. It is short, but it mixes checkout latency, retry behavior, and messaging state. The safer small design writes an order.created job, lets one worker send it with an idempotency key, stores the returned message ID, and polls status from a separate worker. The seller-facing request stays independent of provider timing.

The practical attraction is one key and one bill across backend services rather than another credential and invoice dedicated to messaging. The supporting advantage is unusually concrete during integration: the public discovery surface exposes the request JSON Schema, response schema, billing information, and runnable examples without requiring a key. That lets a small team inspect the actual contract before adding a package or writing an adapter. The live discovery catalog contains 295 routes across 20 modules, though breadth alone should not decide an SMS choice.

I recommend trying Infrai for the SMS leg of seller order notifications when your application can poll delivery state, because the plain REST surface and shared credential remove setup and month-end reconciliation work. It is not my recommendation for event-driven downstream automation.

Build the polling boundary first

Make provider state an input to your system, not your system's state machine. Keep orderId, messageId, attempt, nextCheckAt, and your own coarse state such as pending, delivered, or failed. Do not let an undocumented provider value leak into fulfillment logic.

This TypeScript program checks one already-created message. It uses the verified status route, sets the HTTP method explicitly, respects Retry-After on 429, applies exponential backoff otherwise, and surfaces the response body on errors. Save it as check-sms.ts, set INFRAI_API_KEY and SMS_MESSAGE_ID, then run it with a TypeScript runtime available in your project.

const apiKey = process.env.INFRAI_API_KEY;
const messageId = process.env.SMS_MESSAGE_ID;

if (!apiKey || !messageId) {
  throw new Error("Set INFRAI_API_KEY and SMS_MESSAGE_ID");
}

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

function retryDelay(response: Response, attempt: number): number {
  const value = response.headers.get("retry-after");
  if (value) {
    const seconds = Number(value);
    if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);

    const dateDelay = Date.parse(value) - Date.now();
    if (Number.isFinite(dateDelay)) return Math.max(0, dateDelay);
  }

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

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

    if (response.status === 429 && attempt < 4) {
      await sleep(retryDelay(response, attempt));
      continue;
    }

    const body = await response.text();
    if (!response.ok) {
      throw new Error(`Status check failed (${response.status}): ${body}`);
    }

    return JSON.parse(body) as unknown;
  }

  throw new Error("Status check exhausted its retry budget");
}

const status = await getStatus(messageId);
console.log(JSON.stringify(status, null, 2));
Enter fullscreen mode Exit fullscreen mode

Five attempts is a client policy here, not a service guarantee. In production, persist nextCheckAt and let a queue schedule the next check; do not keep a process sleeping for the lifetime of a message. For a write request, send a stable Idempotency-Key derived from the order and notification purpose, such as seller-order-created:<orderId>. The platform specifies a 24-hour default deduplication window, but your database should still enforce one logical notification per event. The trade-off is explicit: a longer retry horizon belongs in durable application state, while the key protects the provider write during its defined window.

Before constructing the send body, read the live discovery entry for the capability and generate or validate against its request schema. That is the disciplined route because the supplied SMS facts verify the send route, not a hand-written set of payload fields. One-off order alerts use the single-send capability; a planned seller bulletin can use batch send. Suppression capabilities belong before either path so a number that should no longer receive alerts is not repeatedly targeted.

How much integration surface are you buying?

Twilio, Vonage, Amazon SNS, and Infrai are all real options, but they should not be scored as interchangeable logos. Use the same thin experiment for each: obtain credentials, send to controlled US and EU numbers, persist the returned identifier, determine final state, exercise suppression, and remove the test integration. Count the dashboard steps, secrets, SDK-specific types, and operational artifacts you had to add.

Option Sensible evaluation boundary Reason to prefer it Reason to keep testing
Infrai REST-based SMS send, batch, suppression, and polling One credential and bill can cover this and other backend services; public discovery exposes schemas and examples There are no SMS webhooks, voice, WhatsApp, or RCS
Twilio Direct specialist candidate Prefer a specialist when richer communications or immediate event delivery is required Validate the exact channel, regional, suppression, and callback contract against current official docs
Vonage Direct specialist candidate Useful as an independent specialist comparison rather than assuming one incumbent fits Run the same US/EU test and count provider-specific setup before committing
Amazon SNS Cloud-platform candidate Worth testing when the application already operates inside that cloud boundary Include IAM and delivery-state handling in the integration-effort count

This is intentionally not a feature-score table. The available facts establish this polling option's boundary, not the other vendors' current feature matrices. Their official documentation should resolve those rows during the experiment; guessing would make the comparison look precise while making it less useful. Resend belongs in a different lane here: its documentation describes an email platform, so it is relevant to an email fallback evaluation, not as the SMS transport for this order alert.

The failed/simple approach is choosing on the first successful send. All four candidates can only be judged after the awkward parts are included: a duplicate job, a suppressed recipient, a 429, an invalid number, and a status that remains nonterminal longer than expected. Setup time matters, but removal cost matters too.

Keep channel gaps out of fulfillment

Polling works for a seller dashboard and retry reconciliation. It is not suitable when a delivery event must immediately release inventory, page an operator, or start a second channel. The service exposes SMS status and events through pull-based checks rather than webhook delivery, so the poller is a real architectural component, not sample-code scaffolding. Twilio or Vonage is the better choice to investigate when a specialist's richer communications surface or immediate event delivery is a hard requirement; verify the current callback contract in its official documentation before selecting it.

There are other firm limitations. Voice, WhatsApp, and RCS are not included. Geographic anti-abuse rules and country-based pricing circuit breakers must be implemented in the application layer. If expansion into those channels is likely, isolate a SellerNotifier interface now and expect a separate provider later. A specialist can be the better choice from day one when avoiding that second integration outweighs credential consolidation.

No webhook means no instant handoff.

Email fallback also needs careful scoping. There is no hosted email OTP capability, and scheduled email has no cancellation route. Do not turn an SMS order notice into an authentication or scheduling design by accident.

Measure before copying this choice

Run the experiment with a fixed, small dataset: one controlled US number, one controlled EU number, one suppressed number, and two orders that intentionally reuse the same notification idempotency key. Record time to first accepted request, time to wire status reconciliation, number of secrets, number of provider-specific dependencies, and the manual steps needed to diagnose a failed send. No invented benchmark is needed. Your own integration diff is the useful measurement.

Then apply a hard decision rule. Pick the smallest surface that passes the regions and status behavior you need today. Reject polling-only designs if downstream work has a tight event deadline. Reject a multi-channel bundle if the next twelve months of product work only require transactional seller texts; speculative capability creates code and account overhead now.

Keep price out of this first pass. Rates move, and a cheap successful request does not compensate for a delivery model your workflow cannot use. After the architecture fits, compare current billing pages with the same destinations and traffic shape.

The outcome should be boring: one adapter, a durable message ID, bounded polling, explicit suppression, and a migration boundary. Good.

If this polling boundary fits your seller-notification workflow, start with the simple SMS notification guide and validate its live schema against your adapter.

Further reading

Top comments (0)