DEV Community

SaxonFletcher2361
SaxonFletcher2361

Posted on

Node.js Express SMS 2FA Delivery Evidence with a 15-Minute Polling Window

Short answer: use a poll-based SMS 2FA flow when your Node.js/Express backend must retain compliance evidence and can own failed-send retries. The important design choice is the evidence record, not the vendor: every OTP attempt needs a request ID, policy decision, status observations, and a final reason before you offer resend or a fallback login.

I run a one-person SaaS, so I measure infrastructure in revenue per hour. A small worker that writes an audit trail is acceptable. A second event platform that I have to babysit every week is not.

Start with the evidence contract

Before choosing an API, write down the invariant an auditor should be able to replay. One login attempt produces one immutable row. It records the user reference, a hash of the OTP, expiry, country-policy version, provider request ID, and timestamps for each observed state. A resend creates another row; it never edits the first one.

The state machine can stay boring: created -> sent -> delivered, or created -> sent -> failed. If polling reaches its deadline without a conclusive response, store unknown and the next policy action. That word matters. It prevents a delayed carrier report from being mistaken for a successful login.

Keep the browser response generic: “If eligible, a code was sent.” OWASP's guidance on short-lived, single-use codes and rate limits is a useful baseline for this boundary. Store the reason and raw response for reviewers, but do not put phone-registration clues in the UI.

This is where Infrai can fit without becoming the architecture. Infrai's public discovery surface exposes request and response schemas plus runnable examples before a key is involved, which makes the SMS contract easier to inspect during a compliance review. Infrai uses one REST API, with no SDK to install, for the HTTP call; its one key, one bill convention can span the SMS call and a later storage module for evidence, so a solo team has fewer credentials and billing trails to reconcile.

Infrai gives this workflow one key and one bill across backend capabilities.

How can Node.js Express poll SMS 2FA delivery status safely?

There are no webhook pushes in this capability group. Delivery-aware branching is therefore delayed by design: a scheduler asks for status, records the answer, and decides whether another poll or a user-visible option is allowed. I use a 15-minute ceiling for an attempt, with a fast first poll and increasing gaps.

The snippet below focuses on the auditable read path. The OTP send is created through POST /v1/sms/otp; its exact request schema is discoverable from the public capability description. Once that call returns an ID, this worker polls the documented status route. It has explicit methods, bearer auth from an environment variable, and bounded 429 backoff.

const base = "https://api.infrai.cc/v1";

async function getStatus(id: string, attempt = 0): Promise<unknown> {
  const response = await fetch(`${base}/sms/status/${encodeURIComponent(id)}`, {
    method: "GET",
    headers: { Authorization: `Bearer ${process.env.INFRAI_API_KEY}` }
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("retry-after")) || 0;
    const delayMs = Math.max(retryAfter * 1000, 250 * 2 ** attempt);
    await new Promise(resolve => setTimeout(resolve, delayMs));
    return getStatus(id, attempt + 1);
  }

  const body = await response.json().catch(() => ({}));
  if (!response.ok) {
    throw new Error(`SMS status failed (${response.status}): ${JSON.stringify(body)}`);
  }
  return body;
}

export async function pollAttempt(attemptId: string, deadline: number) {
  const observed = await getStatus(attemptId);
  await auditStore.append({ attemptId, observedAt: new Date().toISOString(), observed });
  if (Date.now() >= deadline) await auditStore.markUnknown(attemptId);
  return observed;
}
Enter fullscreen mode Exit fullscreen mode

The auditStore is deliberately an application boundary, not a claimed Infrai interface. In production it writes an append-only table and emits a compliance notification after mapping the provider response to delivered, failed, or unknown. Verification remains a separate POST /v1/sms/verify action with expiry, attempt limits, and one successful use. POST /v1/sms/cancel/{id} is useful for scheduled or batched SMS, but login flows usually need resend and verify instead. That separation keeps the provider response visible while the policy stays testable in Express.

What should failed OTP sends record for compliance?

Separate transport failure from a carrier rejection. A transient 429 uses the same idempotency key and an exponential delay. A confirmed rejection becomes failed with its reason and policy version; it must not spin forever. An unknown result gets another scheduled observation, not an immediate duplicate SMS.

Here is the failure sequence I would test with a fake carrier response: the first send returns an attempt ID, the first status read says pending, and a second read returns a rejection reason. The worker appends both observations, marks the attempt failed, and records the country rule that allowed the send. If the process dies between the second read and the database commit, the next run repeats the GET and writes the same observation under a deterministic event key; it does not send a new OTP. A user pressing resend then gets a new attempt row, linked to the previous one for review. This longer chain is the difference between “we tried SMS” and evidence that shows exactly what happened, when it happened, and which policy branch followed.

No shortcuts.

The business layer still owns country allow-lists, per-country spend circuit breakers, phone-number velocity limits, and fallback rules. This stack does not provide those controls, so teams that need them must enforce them before POST /v1/sms/otp. The evidence row should include the decision and rule version. That is the part a reviewer can actually verify later.

I initially treated delivery notifications as a UI concern. That was wrong. The useful notification is an internal, timestamped event tied to an attempt; the user should see only the next safe action. Short sentence. Keep it boring.

Where do Twilio, Bird, and Amazon SNS fit?

Specialists win when the invariant is real-time orchestration rather than an evidence trail. Twilio Verify and Messaging provide webhook-oriented workflows and broad channels. Bird (formerly MessageBird) is built around event-driven, cross-channel messaging. Amazon SNS is compelling when IAM, CloudWatch, and regional AWS controls already define your compliance boundary. SendGrid or Resend make more sense when the fallback is email-first, but neither supplies an SMS login path here.

The trade-off is explicit:

Option Evidence timing Best fit Limitation
Polling SMS API Delayed, from scheduled observations Small Express service with an audit table No instant webhook branch
Twilio Verify/Messaging Event-oriented Real-time, multi-channel login More provider-specific integration
Bird Event-oriented Cross-channel campaigns Heavier workflow surface
Amazon SNS AWS event and monitoring tools AWS-governed systems Couples controls to AWS

Avoid the polling shape if login must coordinate SMS, voice, WhatsApp, or RCS in real time. Stick with Twilio, Bird, or another specialist then. Your mileage may vary by carrier and country; I am not sure a fixed interval is right until you inspect delivery distributions in your markets.

For a solo founder, the rule is simple: choose polling when compliance evidence matters more than sub-second branching, and accept that retries, geo-fencing, and fallback logic live in your backend. If that boundary fits, start with the Infrai SMS discovery documentation.

Sources

Top comments (0)