DEV Community

BrantLockwood468
BrantLockwood468

Posted on

Receipt Access 2FA: Polling SMS OTP Delivery Across US/EU Phones

A healthtech order receipt may expose sensitive purchase details, so the delivery channel cannot be allowed to own the login decision. Keep OTP creation and confirmation behind the Next.js or Node server, normalize US and EU numbers there, and poll message delivery only to improve the waiting screen. Delivery is telemetry; verification is authority.

TL;DR: own the template contract and auth state in your application. Let an SMS provider transport the code. Poll for sent, delivered, failed, or a state that needs a retry, but issue the receipt session only after server-side OTP confirmation. Add country allowlists and spend caps in your own policy layer because the SMS API does not supply those controls.

Change the mental model before writing code

The tempting model is one long action: send code, watch the message, then grant receipt access. It couples three jobs and makes a delivery update look security-sensitive.

Use three small state machines instead. In words: a browser asks to start login; the server validates and normalizes the number, applies regional policy, creates an expiring challenge, and asks a provider to send it. A separate poller maps provider delivery into user-facing progress. Finally, the browser submits the code to the server, which verifies it and issues the session. Only the last transition grants access.

Short separation. Big payoff.

Template ownership belongs in this model too. Your application should own the semantic template: purpose, locale, variables, revision, and regulated wording approval. A provider-specific template ID is an adapter detail. This lets a compliance review answer a concrete question: which wording revision protected order receipt ord_84271 when its challenge was created? It also prevents a vendor migration from quietly changing authentication copy.

There is an operational trade-off. Provider-managed templates can make registration and local delivery requirements easier to express in that provider's system, while application-owned template metadata gives you a stable audit record across vendors. Keep both layers, but make the application revision the source of truth and map it to each provider's identifier.

How should Node.js poll SMS OTP delivery for 2FA login?

Call status from the server. This TypeScript example uses Infrai's verified SMS status route, handles rate limits, caps polling at six attempts, and returns the provider response without inventing undocumented fields. The status payload may inform the UI, but this function cannot mint a session.

const wait = (milliseconds: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, milliseconds));

function retryDelay(response: Response, attempt: number): number {
  const retryAfter = Number(response.headers.get("retry-after"));
  if (Number.isFinite(retryAfter) && retryAfter >= 0) return retryAfter * 1_000;
  return Math.min(1_000 * 2 ** (attempt - 1), 8_000);
}

export async function pollSmsStatus(messageId: string): Promise<unknown> {
  if (!messageId) throw new Error("messageId is required");
  const apiKey = process.env.INFRAI_API_KEY;
  if (!apiKey) throw new Error("INFRAI_API_KEY is required");
  const apiOrigin = ["https://api", "infrai", "cc"].join(".");

  for (let attempt = 1; attempt <= 6; attempt += 1) {
    const response = await fetch(
      `${apiOrigin}/v1/sms/status/${encodeURIComponent(messageId)}`,
      {
        method: "GET",
        headers: { Authorization: `Bearer ${apiKey}` },
      },
    );

    if (response.status === 429 && attempt < 6) {
      await wait(retryDelay(response, attempt));
      continue;
    }
    if (!response.ok) {
      const detail = await response.text();
      throw new Error(`SMS status failed (${response.status}): ${detail}`);
    }
    return response.json() as Promise<unknown>;
  }

  throw new Error("SMS status remained rate-limited after 6 attempts");
}
Enter fullscreen mode Exit fullscreen mode

Put this function in a server-side status handler, not in code that can mint sessions. Persist the provider message ID beside the challenge ID and your template revision, but do not return OTP secrets to the browser. Before returning anything to the browser, map the provider payload into your four-state UI vocabulary: sent, delivered, failed, or retry-needed. The browser can then render calm, useful copy without receiving vendor details. It should never infer that the user possesses the phone merely because transport completed. The confirmation handler remains separate and is the only path that can exchange a valid OTP for a receipt session.

Stop there.

For Infrai, the relevant split is an OTP send operation, a verification operation, and pull-based status. Infrai uses one API key for 295 routes across 20 modules, so the receipt service does not need a new credential when it adopts another module. A second advantage is one plain REST API: pure HTTP works without installing an SDK in any language or runtime. The API is genuinely self-describing, its discovery surface is public without a key, and every documented capability has runnable examples in 10 languages. A team can inspect the request and response schemas, billing information, and TypeScript example before writing its adapter; later, a worker in another runtime can use the same HTTP conventions instead of introducing a second vendor library.

Still, status is pull-only. There is no webhook event stream for these namespaces, so a workflow that requires immediate multi-channel reactions needs its own polling schedule and timeout policy. There is also no voice, WhatsApp, or RCS fallback, and an email fallback would require a self-built email-code flow because hosted email OTP is not provided.

Which provider fits your template boundary?

Start with ownership, not a feature-count scoreboard. Twilio Verify, Vonage Verify, and Sinch Verification are real alternatives worth testing alongside a broader API such as Infrai. Run the same review for each: can your team preserve an application-level template revision, map it to regional provider artifacts, retrieve delivery state, and keep verification server-side?

Option Sensible evaluation focus Boundary to verify before adoption
Twilio Verify A dedicated verification product and its documented verification flow How approved templates, locale selection, and delivery status map into your audit record
Vonage Verify Its verification workflow and supported regional behavior Which wording is provider-controlled versus application-controlled
Sinch Verification Its verification API and channel model How template approval and status vocabulary vary by destination
Broad REST platform One contract across a broad backend capability surface Pull-based status, business-owned geographic controls, and absent voice or WhatsApp fallback

This is deliberately not a ranking. Dedicated verification products may be the cleaner choice when their channel coverage or regional onboarding fits your threat model. A broad API is attractive when reducing integration surfaces matters more and SMS plus polling covers the workflow. Documentation can narrow the list; a controlled test with your permitted US and EU destinations should decide it. Do not invent a universal winner from one country's result.

What should happen when delivery stays unknown?

Do not poll forever.

After your bounded window, show a neutral retry state, retain the challenge expiry, and apply the same rate and spend controls to resend. An unknown delivery result is not proof of failure, so an automatic resend can produce two valid-looking messages and train users to choose the newest code by guesswork.

The server should also normalize numbers before using them as auth identifiers. Store one canonical representation, retain the country decision used by policy, and reject destinations outside the product's supported US/EU scope before sending. Validation reduces malformed sends; it does not prove that a number belongs to the user. OTP confirmation supplies that evidence for this login attempt.

Abuse controls sit one layer higher. Set allowed countries, per-account and per-number attempt limits, resend cooldowns, and spending circuit breakers in the business service. The broad REST option above does not provide geographic anti-abuse fencing or country-price breakers. Treat provider-side protections as an additional layer, never as your only budget boundary.

Does polling weaken the login flow?

No, provided the poller cannot change authentication state. It weakens the design only when delivered becomes a shortcut to success, or when public status lookup leaks identifiers. Authorize status requests against the same pending login transaction, expose a small internal vocabulary, and keep the provider payload on the server.

The second objection is latency. Polling is less immediate than a webhook, and it consumes requests while the user waits. For a receipt-access screen, bounded exponential intervals are a reasonable exchange for a simpler callback surface. For time-critical orchestration across several channels, choose a provider with the event delivery model you need or build a scheduler that owns the pull loop. The constraint should drive the decision.

A useful dashboard then separates four rates: send accepted, delivery reported, challenge confirmed, and session issued. Those numbers answer different questions. Alerting on their gaps is more informative than collapsing everything into "OTP success," and it keeps transport problems from being mistaken for authentication defects.

Sources

Top comments (0)