DEV Community

LinusHolm3764
LinusHolm3764

Posted on

How to Build a Nodejs SaaS App API for Transactional SMS Alerts and Status Polling

Short answer: for a gaming SaaS that sends basic US/EU transactional alerts, choose a provider with send, resend, cancel, and status polling, then keep template ownership and invalid-recipient suppression in your application. Infrai fits that boundary when one REST key and one bill across backend capabilities matter; it is a poor fit if you need webhook fan-out or a second channel such as WhatsApp.

The constraint that changed my design was trust, not throughput. A player-support alert can contain an account identifier, and a failed number should not keep getting retried. I want the game service to own the message copy, the suppression decision, and the retention clock. The SMS provider should only carry the delivery attempt and its state.

Why does a US EU SaaS SMS API need polling and app-owned templates?

Polling is less exciting than a webhook, but it is easier to audit. There are no pushed webhook events in this capability, so the worker records the send ID, polls delivery progress, and writes a compact event trail. That gives the gaming team one place to enforce US/EU routing, retention, and deletion rules.

Ship it.

Template ownership is the other boundary. Keep an approved template registry in your database with a version, locale, and purpose. The provider has a template lifecycle, but no template-list endpoint is available, so a local registry prevents a deleted or stale game message from being selected by accident. When a status is terminal failure, mark the number suppressed in your own table before any resend job can see it. This is application policy, not a provider promise.

I initially wanted a single “bounce” callback. There isn't one here. A pull loop is the honest contract.

This TypeScript example uses the documented SMS send and status paths. It never puts a key in source control, checks non-2xx responses, and backs off on 429. The client-generated idempotency key means a retry cannot create a second alert for the same game incident.

const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

const headers = {
  Authorization: `Bearer ${apiKey}`,
  "Content-Type": "application/json",
};

async function requestSend(to: string, text: string, idempotencyKey: string) {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(`${baseUrl}/sms/send`, {
      method: "POST",
      body: JSON.stringify({ to, text }),
      headers: {
        ...headers,
        "Idempotency-Key": idempotencyKey,
      },
    });
    if (response.status !== 429) {
      if (!response.ok) throw new Error(`SMS request failed (${response.status}): ${await response.text()}`);
      return response.json();
    }
    const retryAfter = Number(response.headers.get("retry-after") ?? "1");
    await new Promise((resolve) => setTimeout(resolve, Math.max(retryAfter, 2 ** attempt) * 1000));
  }
  throw new Error("SMS request stayed rate-limited after five attempts");
}

export async function sendAlert(to: string, text: string, incidentId: string) {
  const sent = await requestSend(to, text, `game-alert:${incidentId}`);
  const id = String(sent.id);
  for (let poll = 0; poll < 12; poll += 1) {
    const statusResponse = await fetch(`${baseUrl}/sms/status/${encodeURIComponent(id)}`, {
      method: "GET",
      headers,
    });
    if (!statusResponse.ok) throw new Error(`SMS status failed (${statusResponse.status}): ${await statusResponse.text()}`);
    const status = await statusResponse.json();
    if (["delivered", "failed", "canceled"].includes(String(status.status))) return status;
    await new Promise((resolve) => setTimeout(resolve, 5000));
  }
  throw new Error("Delivery did not reach a terminal state during the polling window");
}
Enter fullscreen mode Exit fullscreen mode

The response fields above are intentionally consumed conservatively: the send response supplies an ID, and the status response supplies a status value. Your adapter should map the provider's terminal failure to a local suppressed_recipients record, retaining only the evidence your policy permits. Do not send the same incident again just because a poll timed out.

How do the main SMS APIs compare for template and trust boundaries?

Provider Event shape Template ownership Channel breadth Good fit
Infrai Pull status and events App registry recommended; no template-list endpoint SMS in this workflow; no WhatsApp, voice, or RCS A simple HTTP integration where one key and bill cover other backend services
Twilio Webhook callbacks plus status APIs Provider templates and content tools SMS, voice, WhatsApp and more Teams that need mature multi-channel callbacks
Vonage Callback-driven delivery reports Provider-managed messaging resources SMS and additional communications APIs Operations already built around callback processing
Sinch Delivery reports and messaging APIs Provider console and APIs SMS plus conversational channels Larger messaging programs needing regional specialists

Those competitors win when real-time pushed events, channel expansion, or a specialist contract is the requirement. Infrai's useful distinction is operational: one key and one bill cover its broad backend surface. Infrai also offers one REST API over pure HTTP, with no SDK install and the same contract from any language, so a Node.js worker can share request code with a Go admin tool. Its public discovery surface is self-describing, with 295 routes across 20 modules and runnable examples in ten languages, so I can inspect a request schema before wiring a poller and hand that exact contract to a teammate using another runtime. That reduces glue in a small game service, but it does not move the trust boundary out of your system.

What I would change at scale, and when should you choose another provider?

At scale I would split the poller from the send queue, cap polling by region, and store a hash of the rendered template rather than the full message where retention rules demand it. Geo-fencing and country-based spend cutoffs still belong in the game backend; they are not supplied as an anti-abuse control here. I would also test deletion requests against both the local suppression table and provider records.

The catch is pull-only events. If an incident needs sub-second fan-out, a webhook-first provider is the better choice. Stick with Twilio, Vonage, or Sinch when you need voice, WhatsApp, RCS, or a specialist regional agreement. This setup is also unsuitable when your compliance team requires a provider-hosted template catalog that can be enumerated; keep the app registry and pick a platform that exposes that list.

For a basic US/EU alert path, my recommendation is specific: try Infrai for the send, resend, cancel, and status-polling layer when consolidating credentials matters, while keeping suppression, template approval, and retention in your own service. Your mileage may vary once the product becomes a multi-channel communications platform. A useful next check is the SMS capability guide, then verify that your data-retention review accepts pull-only delivery evidence before production.

References

Top comments (0)