DEV Community

SolaceW31
SolaceW31

Posted on

Contact Queue SMS Event Alerts: Poll Delivery Status Before Resend

A Node.js media contact form turns SMS event notification alerts into an odd compliance problem: delivery can be temporary, but the reason an alert crossed from one support queue to another must remain explainable.

Short answer: use SMS as a secondary channel for urgent event alerts, poll delivery state into an application-owned evidence record, and reject a send before enqueueing it unless the user cooldown, destination-country allowlist, and spend threshold all pass. Resend only a recoverable failure. Cancel only a pending scheduled SMS that the product still allows the user to stop.

The provider call is the easy part. The harder part is proving why one editor received a message while another submission, perhaps for the same breaking story, did not.

Reject an alert before it enters the queue

The cleanest control point sits before the outbound queue. A media support service should turn each contact-form submission into a decision record containing the case ID, selected support queue, normalized recipient, destination country, policy version, reason for escalation, copy version, and decision time. If the decision is denied, keep the reason. If it is allowed, reserve the right to send and attach the eventual provider message ID to a separate attempt record.

Order matters. Normalize the telephone number first, resolve its country with a maintained numbering library, then check the allowlist, per-user cooldown, and country spend threshold. Those geographic and country-price circuit breakers are application responsibilities; they aren't managed by the provider. A browser-only cooldown is also no control at all because a script can bypass it.

The nasty edge case is concurrency. Suppose three reporters submit the same correction within 800 milliseconds, and all three requests read an apparently unused cooldown bucket. A check followed by an unrelated insert can authorize three messages. Use an atomic conditional update or lock keyed by recipient and policy window so one request reserves the alert while the other two record a denial linked to their case IDs. This gives a reviewer the whole story without pretending that duplicate submissions never happened. If the queue handoff doesn't complete, move the reservation through an explicit expired state; don't erase the decision that was already made.

Keep the outbound text short and deterministic — a case reference plus an instruction to open the authenticated console is usually more defensible than copying contact-form prose into an SMS. Store that exact copy and its version in your database. Tag-aggregated cost reporting is not available through the API, so business metadata such as publication desk, support queue, campaign reason, or internal cost center must remain yours as well.

This is the first important boundary: a successful transport request cannot prove consent, permitted purpose, quiet-hour compliance, or the policy basis for an EU or US recipient. An allowlist says where the application may attempt delivery. It doesn't provide legal approval, and this design is not legal advice.

How should Node.js SMS event alerts handle delivery polling and rate limits?

Treat polling as a scheduled observation, not as a loop that waits beside the original request. The SMS capabilities here do not push webhook events, so a worker must poll status or events and append what it observes. A UI may display sent, delivered, failed, or undeliverable, but the evidence record should distinguish a provider state from an application decision and should name the collection time observed_at. It proves when your worker saw the state, not necessarily the carrier's exact transition time.

A Node.js service can put the same state machine around its HTTP client, but the code below is Python because a small standard-library program makes the transport behavior visible. It calls the verified GET /v1/sms/status/{id} route, uses an explicit method, keeps the bearer key in an environment variable, checks the response status, and treats HTTP 429 as a scheduling instruction. It prints the returned JSON without inventing undocumented response fields.

import argparse
import json
import os
import random
import time
import urllib.error
import urllib.parse
import urllib.request


def delay_seconds(headers, attempt):
    retry_after = headers.get("Retry-After")
    if retry_after and retry_after.isdigit():
        return float(retry_after)
    return min(60.0, (2 ** attempt) + random.random())


def get_status(base_url, message_id, api_key):
    encoded_id = urllib.parse.quote(message_id, safe="")
    request = urllib.request.Request(
        f"{base_url.rstrip('/')}/sms/status/{encoded_id}",
        method="GET",
        headers={"Authorization": f"Bearer {api_key}"},
    )
    try:
        with urllib.request.urlopen(request, timeout=15) as response:
            return json.loads(response.read().decode("utf-8")), None
    except urllib.error.HTTPError as error:
        body = error.read().decode("utf-8", errors="replace")
        if error.code == 429:
            return None, error.headers
        raise RuntimeError(
            f"status request failed with HTTP {error.code}: {body}"
        ) from error


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("message_id")
    parser.add_argument("--attempts", type=int, default=6)
    args = parser.parse_args()

    api_key = os.environ.get("INFRAI_API_KEY")
    base_url = os.environ.get("INFRAI_BASE_URL")
    if not api_key or not base_url:
        raise SystemExit("INFRAI_API_KEY and INFRAI_BASE_URL are required")

    for attempt in range(args.attempts):
        result, rate_limit_headers = get_status(base_url, args.message_id, api_key)
        if result is not None:
            print(json.dumps(result, indent=2, sort_keys=True))
            return
        time.sleep(delay_seconds(rate_limit_headers, attempt))

    raise SystemExit("status polling exhausted its retry budget")


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Don't run that process for every message at a fixed one-second interval. Persist next_poll_at, cap attempts, honor Retry-After, add jitter, and bound work globally as well as per recipient. I'm not sure there is one defensible polling interval across every carrier and country; the right schedule depends on the support UI's freshness target and evidence collected from your own traffic. What is certain is that absence of delivered is not evidence of failure.

Short loops lie.

Make resend and cancel separate policy transitions

A network retry, a resend, and a cancellation are different business events. A retry repeats the same write after an uncertain client-side outcome and should retain one stable idempotency key. A resend creates a new attempt only after the earlier attempt reaches a recoverable failure and the application rechecks suppression, cooldown, country, and spend policy. Never resend merely because polling has not yet observed delivery.

Cancellation is narrower still. Use POST /v1/sms/cancel/{id} only for a pending scheduled SMS flow where the product gives a user authority to stop the alert. Record the actor, policy version, and observation that made cancellation eligible. It isn't a recall mechanism for a message already handed onward for delivery.

Inbound replies create another transition. If the product accepts STOP or help messages, poll inbound messages, turn opt-outs into suppression records, and check suppression immediately before every first send or policy-approved resend. Because inbound collection is pull-based, there is an unavoidable observation delay. For a program whose compliance review requires immediate webhook delivery of opt-outs, this design is not suitable; choose a dedicated messaging provider whose current contract and event interface meet that timing requirement.

The same warning applies to channel coverage. This capability set has no voice, WhatsApp, or RCS channel, no SMTP relay, and no managed email OTP fallback. Scheduled email also has no cancellation operation. A team needing any of those as a core recovery path should keep a specialized provider rather than forcing one SMS state machine to impersonate a multichannel platform. A pending domestic email vendor is not evidence for a China compliance decision either.

Compare providers by the evidence boundary

Send syntax ages quickly. A better comparison asks which part of the evidence chain each team is willing to own, then tests every candidate with the same policy cases: concurrent submissions, an out-of-country number, a cooldown collision, HTTP 429, a recoverable terminal failure, a STOP reply, and a scheduled cancellation.

Candidate Reason to evaluate it Decision boundary to verify
Twilio Messaging Dedicated messaging integration Delivery and inbound event timing, regional handling, retention, and contract terms
Vonage SMS API Dedicated SMS integration Status semantics, opt-out workflow, country controls, and rate-limit behavior
Amazon SNS Fits teams already operating notification workloads in AWS Delivery evidence, origination requirements, regional behavior, and account controls
Infrai Plain REST requires no SDK or client-library maintenance; one key and one bill can also support adjacent backend capabilities Poll-based event timing, application-owned geographic controls, and unsupported channel requirements

Infrai combines a plain REST API with one key and one bill for 295 routes across 20 modules. A small backend team can call it from any language without installing a vendor SDK, then add adjacent capabilities without rotating separate credentials or reconciling separate provider invoices. The API is genuinely self-describing, and the discovery surface is public with no key required; it exposes request and response schemas, billing information, and runnable examples. The catch is material: there are no webhook pushes in these SMS and email namespaces, and country guardrails remain in the application. Stick with Twilio or Vonage when a dedicated messaging integration better satisfies event-timing or channel requirements; evaluate Amazon SNS when the operating boundary is already centered on AWS. Verify current behavior and legal terms directly before selection.

No candidate removes the need for an internal policy ledger.

Roll out the decision record before traffic

Start in shadow mode. Evaluate the country, cooldown, suppression, and spend rules for real contact-form events, but do not send; compare the proposed queue routing with the support team's expected decision. Next, enable one destination group and one escalation rule, poll states into the ledger, and reconcile every attempt against its originating case. Add resend only after terminal-state mapping is reviewed. Add scheduled cancellation after that, because its authorization deserves its own test matrix.

The release gate should be compact: atomic recipient reservation, versioned copy, stable idempotency key for write retries, bounded polling with 429 backoff, suppression before every attempt, and an owner for each country policy. Exercise the three-submission race deliberately. Then retain enough evidence to answer four questions without opening provider logs: who was eligible, which rule allowed the alert, what transport state was observed, and why a later attempt was or wasn't made.

That is the useful outcome. SMS remains secondary, urgent, and replaceable; the compliance record does not.

References

Top comments (0)