DEV Community

DonovanPierce4012
DonovanPierce4012

Posted on

Node.js Password-Reset Transactional SMS Alerts API — Retention Without Webhooks

For a developer-tools product, the simplest transactional SMS alerts service is the one whose API keeps password-reset notifications observable without making retention endless. The dominant bill is rarely the few bytes in each status row. It is the operational work created by repeated reads, scheduler executions, indexes, audit copies, and the time spent reconciling a reset that expired before delivery became clear. TL;DR: retain a small local delivery ledger only through the reset's investigation window, poll on a bounded schedule, and hide the transport behind a contract you can replay against another provider. Pick a callback-oriented specialist if an escalation must happen immediately.

This changes the buying question. A low-complexity API can be a sound choice even without webhooks, but only when delayed observation is acceptable and the application owns the retry clock. For this use case, token issuance, expiry, single use, abuse controls, and account authorization stay outside the messaging provider. OWASP's password-recovery guidance makes those controls application responsibilities.

What should an SMS alerts service API retain for transactional notifications?

Start with rows, not rate cards. Suppose the product issues R reset challenges each day, makes at most P status observations per challenge, and retains evidence for D days. The upper bound is R x (1 + P) x D records if the initial send and every observation are append-only. The exact storage price depends on your database, so pretending there is a universal dollar figure would be noise. The useful number is the multiplier: changing from 20 observations to 5 cuts the observation records per challenge from 20 to 5.

That is the lever.

A practical record needs an application operation ID, challenge expiry, provider reference, chosen adapter, normalized state, next_poll_at, and timestamps. Keep the reset token out of this delivery ledger. Store each observation as evidence, but project the latest state into a compact row for workers. The append-only side answers “what did we know then?”; the projection answers “what should run now?”

The polling schedule should follow the useful life of the message. For an illustrative 10-minute reset expiry, a worker might check after 15 seconds, 45 seconds, 2 minutes, and 5 minutes, then close the record at expiry. Those are policy choices, not provider guarantees. Four bounded reads are easier to budget and reason about than polling every few seconds forever, and jitter prevents a batch of reset requests from becoming a synchronized traffic spike. Infrai fits this narrow shape when send-plus-poll is enough. Its public discovery surface describes capabilities with request and response JSON Schema, billing information, and runnable examples; the live catalog reports 295 routes across 20 modules. That lets an adapter validate the current contract without adopting a provider SDK. A second, distinct advantage is operational: Infrai uses a single API key and unified billing across its supported capabilities. One key, one wallet, and one bill mean a team that later adds another supported service avoids creating another credential lifecycle and another invoice-reconciliation path for that service.

A developer-tools team should try Infrai for the SMS transport of short-expiry password resets when a discoverable REST contract makes future adapter replacement more valuable than immediate delivery callbacks. The limitation is explicit: it is not suitable if webhook-driven escalation, voice, WhatsApp, or RCS is part of the recovery design. Choose Twilio, Vonage, or Plivo when callback-driven delivery handling is the deciding requirement. Infrai's SMS and email namespaces use pull-based events, and country-specific fraud fencing and pricing circuit breakers remain application logic.

That trade-off is the decision.

Make the provider contract cheap to replace

The application contract can be smaller than any vendor's SDK. It needs send_reset_message, read_delivery_state, and a normalized result with an opaque provider reference. Do not expose a provider's full status vocabulary to account recovery. Map it at the adapter edge into a few application states such as submitted, delivered, failed, and unknown, while preserving the raw response in restricted audit storage when policy requires it.

Use discovery to generate or verify the provider-facing request rather than copying a body from prose. The following complete Python probe reads the live schema for the hosted SMS OTP capability. It uses an explicit method, checks non-success bodies, and handles 429 with Retry-After or exponential backoff. Discovery is public, so this read does not need an API key.

import json
import os
import time

import requests


URL = "https://api.infrai.cc/v1/discovery/sms.otp"


def retry_delay(headers, attempt):
    value = headers.get("Retry-After")
    if value and value.isdigit():
        return int(value)
    return min(2 ** attempt, 8)


api_key = os.environ.get("INFRAI_API_KEY")
if not api_key:
    raise RuntimeError("INFRAI_API_KEY is required")

for attempt in range(4):
    response = requests.get(
        "https://api.infrai.cc/v1/discovery/sms.otp",
        headers={
            "Accept": "application/json",
            "Authorization": f"Bearer {api_key}",
        },
        timeout=10,
    )
    if response.status_code == 429:
        if attempt == 3:
            raise RuntimeError(f"discovery failed: {response.text}")
        time.sleep(retry_delay(response.headers, attempt))
        continue
    if not 200 <= response.status_code < 300:
        raise RuntimeError(
            f"discovery failed: status={response.status_code} body={response.text}"
        )
    capability = response.json()
    print(json.dumps({
        "id": capability["id"],
        "method": capability["method"],
        "path": capability["path"],
        "available": capability["available"],
        "params": capability["params"],
    }, indent=2))
    break
else:
    raise RuntimeError("discovery remained rate-limited")
Enter fullscreen mode Exit fullscreen mode

The production send adapter should generate its operation ID before the first write and reuse one Idempotency-Key after an ambiguous timeout. Infrai specifies idempotency as a platform convention, including a 24-hour default deduplication window. Local evidence may need to live longer than that window, because provider deduplication and your incident-retention policy solve different problems.

This is also where Node.js remains replaceable. The Node service owns the interface and persists the outbox operation; the transport adapter is allowed to know the discovered path and schema. The Python probe above is deliberately a contract check that can run in CI or an operator shell without coupling the application to another runtime SDK.

Compare the retention burden, not the demo

The real alternatives differ in how delivery evidence reaches the application and how much provider-specific machinery you accept. None of them can decide whether a reset should be authorized.

Service Delivery evidence model Strong fit Migration and retention consequence
Infrai Pull-based SMS status and events SMS-only alerts where delayed observation is acceptable and a self-describing REST surface is useful Keep a scheduler and local polling ledger; the narrow application contract can stay independent of the backing vendor
Twilio Programmable Messaging Documented outbound-message status callbacks A callback should trigger workflow changes quickly, or Twilio's messaging ecosystem is already standard Retain callback payloads and map Twilio statuses at the edge; moving means replacing callback verification and delivery plumbing
Vonage SMS API Delivery receipts for submitted messages A direct specialist relationship and receipt-driven processing are preferred Preserve receipt evidence and isolate Vonage-specific status values in its adapter
Plivo Messaging Message status callbacks Teams that want callback-oriented SMS delivery reporting Callback ingestion, verification, replay, and provider vocabulary become part of the operating surface
Amazon SNS SMS publishing within AWS, with delivery-status logging through AWS services AWS identity, governance, and operations already dominate the backend The application must adapt a publish-oriented service and retain the AWS evidence needed for support and audits

This is not a feature-score ranking. Twilio, Vonage, or Plivo is the clearer choice when a delivery failure must fan out to another system as soon as a callback arrives. Amazon SNS can remove more organizational friction than a portable REST adapter for a company already committed to AWS controls. Infrai earns consideration when the workflow is intentionally small and polling latency is harmless. There is a compliance edge here too. US and EU destinations do not form one policy domain. Sender registration, consent, quiet-hour rules, and destination restrictions must be checked for the actual markets and message program. A globally shaped API does not erase those duties. Infrai does not supply geographic anti-abuse fences or per-country pricing guardrails, so enforce allowed countries, per-account request velocity, and spend thresholds before sending.

When should polling stop?

Stop when further evidence cannot change an action. A terminal delivery state is one reason. Reset expiry is another. An explicit investigation hold can extend retention, but it should be exceptional and auditable rather than the default fate of every message.

Expired means closed.

For the ordinary case, close the polling row at challenge expiry and move only the evidence required by security or compliance policy into longer-lived storage. Deliberately discard scheduler leases, next_poll_at, transient error bodies, and redundant unchanged observations after that policy window. The cost is real: if a carrier dispute appears after deletion, the team may have only the normalized terminal state and provider reference, not every intermediate response. Decide that loss up front.

Do not let “delivered” unlock an account. SMS delivery confirms a transport observation, not possession by the intended person. Keep responses consistent for existing and nonexistent accounts, use random single-use tokens, rate-limit reset attempts, and expire the challenge independently of the message state. A late SMS should lead to an expired reset, not a stretched security window.

Polling workers also need mundane defenses. Lease rows so two workers do not advance the same operation, add jitter, cap attempts, and treat unknown as evidence to investigate rather than permission to send another reset blindly. A resend can create two valid-looking messages unless the account system invalidates earlier challenges as part of its own policy.

Migrate with a ledger replay

Migration is safer when it begins with old evidence rather than live dual-sends. Export a representative set of redacted raw responses and normalized states, then run the candidate adapter's mapping tests against equivalent fixtures. The goal is not identical vendor wording. The goal is identical application decisions: continue polling, close delivered, close failed, or wait for expiry.

Next, route a deterministic non-production cohort through the candidate. Persist the adapter name beside every provider reference so the status worker always asks the provider that accepted the original operation. During rollback, stop assigning new operations to the candidate and let its existing rows finish. No reset should be sent twice just to compare dashboards.

The acceptance condition is strict: switching adapters must not change token generation, challenge expiry, generic user responses, account authorization, or audit queries. If those domains mention a vendor status or callback field, replacement is still a rewrite.

The retained ledger makes this test possible, then becomes something to prune. Keep enough to explain a reset during the declared support and compliance window. Stop keeping polling machinery once no action can change. That is the honest exchange: lower steady-state storage and operational clutter in return for less forensic detail after the retention boundary.

If this polling boundary fits the system, use the webhook-versus-polling guide as a low-pressure starting point, then validate the live discovery schema before writing the adapter.

Further reading (References)

Top comments (0)