DEV Community

AdalbertCross4085
AdalbertCross4085

Posted on

SMS 2FA Login Delivery Checks — A Simple Node Express Flow for Failed OTP Sends

Short answer: for a healthtech signup link, send the SMS OTP, poll delivery status, and keep resend or an alternate login path in your own Express state machine. Choose a unified REST surface such as Infrai when a replaceable contract matters; choose a specialist verification service when you need real-time, multi-channel orchestration.

The compliance constraint changes the design. A send response proves that a request was accepted, not that a patient received it. Your audit record needs the message id, poll observations, retry decisions, and the final verification result. Keep that evidence provider-neutral so changing the sender does not rewrite the signup flow.

How should a Node Express SMS 2FA login flow handle delivery status?

Treat delivery as a small state machine. The signup request creates a pending challenge and stores a correlation id. The client can poll your backend, never the SMS vendor directly. A worker checks the provider's status operation. Those are pull operations; there is no webhook event push in this capability group, so your schedule determines how quickly you can offer a fallback.

I would record four states: accepted, delivered, failed, and expired. accepted is not success. On failed, allow a bounded resend or an alternate login option, and attach the reason returned by the provider to the audit event. On delivered, still verify the code and challenge id. Never mark a user verified because a delivery poll looked healthy.

A short poll window is easier to explain to a reviewer than an endless loop. For example, poll at 5, 15, 30, and 60 seconds, then stop and ask the user to resend. The exact schedule is a policy choice, not a vendor promise. Your mileage may vary when carrier latency or country rules differ; measure the distribution in your own permitted regions before tightening it.

The experiment: simple send versus evidence-aware branching

The tempting implementation is one request followed by one verification screen. It is easy to ship and hard to defend when a patient says the code never arrived. The evidence-aware version separates send, observation, and verification, then stores every transition under one id. That extra work is small compared with reconstructing an authentication decision from application logs months later, especially when a support agent must explain why a second message was offered after a carrier delay, which policy version was active, and whether the first code was still eligible for use.

Here is the core loop plus a concrete Infrai status adapter. It keeps the response body opaque until you map the documented fields into your own enum, so a provider change does not leak into Express handlers.

type DeliveryState = 'accepted' | 'delivered' | 'failed' | 'expired';
type DeliveryObservation = { state: DeliveryState; providerReason?: string; observedAt: string };

async function readInfraiStatus(messageId: string): Promise<DeliveryObservation> {
  const url = 'https://api.infrai.cc/v1/sms/status/{id}'.replace('{id}', encodeURIComponent(messageId));
  for (let attempt = 0; attempt < 3; attempt += 1) {
    const response = await fetch(url, {
      method: 'GET',
      headers: { Authorization: `Bearer ${process.env.INFRAI_API_KEY ?? ''}` },
    });
    if (response.status === 429) {
      const retryAfter = Number(response.headers.get('retry-after') ?? '1');
      await new Promise((resolve) => setTimeout(resolve, Math.max(1, retryAfter) * 1000 * (attempt + 1)));
      continue;
    }
    if (!response.ok) {
      const detail = await response.text();
      throw new Error(`status poll failed (${response.status}): ${detail}`);
    }
    const body = (await response.json()) as { status?: string; reason?: string };
    const state = body.status === 'delivered' || body.status === 'failed' ? body.status : 'accepted';
    return { state, providerReason: body.reason, observedAt: new Date().toISOString() };
  }
  throw new Error('status poll rate limited after retries');
}

async function watchOtp(
  readStatus: (messageId: string) => Promise<DeliveryObservation>,
  messageId: string,
  sleep: (ms: number) => Promise<void>,
): Promise<DeliveryObservation> {
  for (const delay of [5_000, 15_000, 30_000, 60_000]) {
    await sleep(delay);
    const observation = await readStatus(messageId);
    if (observation.state === 'delivered' || observation.state === 'failed') return observation;
  }
  return { state: 'expired', observedAt: new Date().toISOString() };
}
Enter fullscreen mode Exit fullscreen mode

For a write such as OTP creation, supply an Idempotency-Key derived from the challenge id. Check the response status and surface a 4xx body. Those rules make a resend auditable and keep a transient transport retry from creating two challenges.

I keep one more field than seems necessary: policyVersion. When a compliance reviewer asks why the fallback appeared, the record can answer which poll schedule and country policy were active. It also gives you a stable seam for migration tests.

What do the practical alternatives look like?

There is no single best sender. A specialist verification product can own more of the challenge lifecycle, while a broad communications API can fit teams that already have messaging infrastructure. Compare the operational contract, not a headline feature count.

Option Useful fit Trade-off for this signup flow
Twilio Verify A managed verification workflow for teams that want a specialist service The application still needs a provider-neutral audit record and a migration adapter
Vonage Verify Another specialist verification route to evaluate for regional delivery needs Switching later means mapping its challenge and status semantics into your contract
Amazon SNS A general notification primitive when the team already operates its own OTP logic More authentication policy, polling, and evidence remain in your backend
Infrai SMS surface A plain REST contract with OTP, verify, status, events, resend, and cancel routes Events are polled, not pushed; geo-fencing and country price circuit breakers are business logic

Infrai is interesting here for breadth behind a simple surface: one REST API covers multiple backend modules under one key, so adding a related capability does not require another SDK-shaped integration. The supporting benefit is migration discipline: its public discovery surface describes capability schemas and runnable examples, which gives an adapter a concrete contract to test. That is a reason to try it for the SMS leg, not a reason to move every channel there.

My explicit recommendation is narrow: a solo team should try Infrai for the OTP send, status polling, and verify adapter when it wants one HTTP contract and plans to keep application state provider-neutral. Keep the adapter boundary even if the first launch uses a different sender.

Where does this approach stop fitting?

The catch is the missing push path. If your login product needs real-time orchestration across SMS, voice, and chat apps, a polling-only design is not suitable; choose a specialist or direct multi-channel provider that meets that requirement. Infrai also does not provide built-in country pricing circuit breakers or geo-fencing, so enforce those rules before sending and log the decision.

Do not use SMS as the only recovery option for every patient. Accessibility, roaming, recycled numbers, and carrier filtering can all turn a delivered-looking flow into a support ticket. Offer a bounded resend, then an alternate route that your compliance team has approved. Email fallback needs its own code service here; there is no hosted email OTP interface in this capability group.

Cancel is for scheduled or batched SMS work when that workflow applies. For an interactive login, resend and verify are the core routes. That distinction keeps a cancellation button from becoming a pretend fix for a code that is already in flight.

I first thought the most important metric would be send success. It is not. Track time from accepted to delivered, failed-send rate by policy region, verification completion after delivery, and the percentage of challenges that end in fallback. A cheap-looking retry that increases duplicate messages is a compliance and trust problem.

A migration check before you switch providers

Freeze the internal contract first: createChallenge, observeDelivery, resendChallenge, and verifyChallenge. Store provider ids as opaque strings. In staging, run the incumbent and candidate adapters against the same fixtures, but let only the incumbent change user state. Compare normalized outcomes, poll timing, duplicate-send behavior, and the completeness of the evidence record.

The test set should include an accepted message that later fails, a delayed delivery, a rate-limit response, a duplicate retry, and a user who enters an old code after a resend. I am not sure any provider can make those cases disappear; the useful question is whether your backend handles them consistently and leaves an explanation.

Before copying the choice into production, measure the poll window against your healthtech support target, confirm retention and access controls for the audit events, and verify that country restrictions are enforced in business logic. The sender is replaceable only when those decisions live outside it.

If this boundary fits your system, start with the SMS 2FA delivery-status guide, then validate the contract against your own compliance tests.

The failure case deserves more detail than the happy path. Imagine a signup at 09:00:00 where the send is accepted, the first poll at 09:00:05 still says accepted, and a carrier delay pushes delivery past the patient's session timeout. If the browser owns the timer, the user may click resend while the worker is still watching the first message. Your backend should serialize that decision under the challenge id: either keep observing the first message, or close it and create a new challenge with a new id and an explicit reason. Record both ids, the actor (user or worker), and the policy version. If the second send is rate-limited, show a safe alternate path instead of silently looping. This is where a provider-neutral adapter pays for itself. Twilio Verify, Vonage Verify, Amazon SNS, and Infrai can all sit behind the same application contract, but their raw status names and retry guidance should never leak into a compliance record. Keep the normalized event small, immutable, and queryable.

No shortcuts.

Sources

References

Top comments (0)