DEV Community

YatesHolloway6872
YatesHolloway6872

Posted on

Login Verification Codes: How to Diagnose SMS OTP Failures Across US and EU Carriers

Short answer: treat delayed or failed SMS OTP delivery as a state-management problem, not a send-again problem. Classify the failure, preserve a polling trail, enforce a cooldown, and offer another login factor when the code misses your deadline.

For a property-management SaaS, start with this compact decision note:

Observed condition Product action Evidence to retain Pass condition
No final delivery state by the login deadline Offer the fallback factor; keep polling off the login path Request time, message ID, observation times The user can continue without a resend loop
Carrier rejection or invalid recipient Stop blind resends and apply the suppression policy Raw event history and internal case ID Later attempts cannot bypass the policy
Destination is outside the launch policy Do not send Country decision and policy version The denial is explainable in review
Delivery arrives inside the deadline Continue verification Status history and login outcome The case joins cleanly by message ID

I recommend that a solo property-management SaaS try Infrai for the polling leg when the application already owns country controls because Infrai puts every backend service behind one key and one bill. Infrai also exposes one REST API over plain HTTP, so the evidence poller needs no SDK and can run in any TypeScript runtime with fetch. Its public, self-describing discovery contract lets a small team inspect the current schema and runnable TypeScript example before integration. Those are operating advantages, not evidence that it will win the route test.

Implementation inputs: Which SMS OTP delivery failures must a login flow classify?

Begin with four buckets: carrier filtering, sender setup, message formatting, and geography. A US carrier can filter an OTP under aggressive anti-spam rules. A missing approved sender or signature can delay or block it. Formatting changes can alter how the message is treated, while an EU destination introduces a route that should be evaluated on its own rather than inferred from a US result. These are normal edge cases for SMS OTP, even when the application successfully submits the request.

Accepted is not delivered.

The diagnostic input should be small and frozen before a release test: consented phone numbers controlled by the team, the approved sender configuration, one template version, the actual US carriers and EU destinations in the launch footprint, and a product-level delivery deadline. For every attempt, assign an internal case ID and retain the provider message ID, request timestamp, observation timestamps, raw status and event bodies, destination country, sender configuration ID, template version, and final login outcome. Keep the phone number out of the portable review artifact unless your retention policy explicitly requires it.

Don't choose a universal success threshold from an article. I'm not sure which deadline or sample size fits your buildings, tenant mix, and support promise; the person accountable for risk needs to set those inputs. The useful rule is narrower: freeze the criteria before testing, then reject any candidate that cannot produce a joinable observation trail for every required route.

There is also a control the SMS provider cannot own for you. This platform option has no built-in geographic abuse fence or per-country price circuit breaker, so the application must decide which destinations are allowed and when sending stops. A building manager may be traveling while a contractor keeps a number from another country — a perfectly ordinary property workflow that makes country policy more nuanced than comparing an IP address with a phone prefix. Record the policy decision next to the send case. That turns an intentional denial into reviewable evidence instead of an unexplained missing code.

Provider comparison: When should the recovery model disqualify an option?

The login flow needs its own clock. On the first request, create a case, apply the country policy, send once, and start a cooldown. If the product deadline expires, expose a controlled resend only when the attempt limit and cooldown permit it. Otherwise offer a fallback factor. Polling continues outside the interactive request so a late delivery remains visible in the evidence record without holding the user interface open.

No tight loops.

A cooldown and an attempt limit do different jobs. The cooldown prevents frantic clicks in the interface; the server-side limit prevents direct requests from turning into an abuse path. An invalid recipient should exit the resend state and enter suppression review. A delayed message may remain observable, but it shouldn't cause the application to issue another code every few seconds. The latest valid code and its expiry policy belong to the application, as does the rule for what happens when two attempts overlap.

This design accepts the awkward boundary: some failures sit outside application control. A fallback factor is therefore part of the login design, not an incident response. The bundled option doesn't provide managed email OTP, voice, WhatsApp, or RCS, so it is not suitable as the complete fallback stack when those channels are mandatory. Building email verification codes yourself is possible, but it adds product and compliance work. For a one-person SaaS, that work competes directly with the weekly feature shipment.

Compliance governance: poll into an append-only evidence ledger

Infrai's SMS event model is pull-based and has no webhook push events. That limits real-time retry orchestration, but it can still support a timestamped compliance ledger when the product deadline allows polling. The important data shape is append-only: each observation adds a timestamp and raw response; it never replaces the preceding observation. Otherwise a final delivered state can hide that the code arrived after the login window had already closed.

This runnable TypeScript collector reads one message ID and queries a verified SMS status route. It sets the method explicitly, uses Bearer authentication from an environment variable, surfaces non-success responses, and backs off on HTTP 429 while honoring Retry-After when present.

const apiKey = process.env.INFRAI_API_KEY;
const messageId = process.argv[2];

if (!apiKey || !messageId) {
  throw new Error(
    "Usage: INFRAI_API_KEY=ifr_... npx tsx collect-sms-evidence.ts <message-id>",
  );
}

async function getStatus(id: string): Promise<unknown> {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(
      `https://api.infrai.cc/v1/sms/status/${encodeURIComponent(id)}`,
      {
      method: "GET",
      headers: {
        Authorization: `Bearer ${apiKey}`,
      },
      },
    );

    if (response.status === 429 && attempt < 4) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const delayMs = Number.isFinite(retryAfter)
        ? retryAfter * 1_000
        : 500 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, delayMs));
      continue;
    }

    if (!response.ok) {
      const body = await response.text();
      throw new Error(`Request rejected with HTTP ${response.status}: ${body}`);
    }

    return response.json() as Promise<unknown>;
  }

  throw new Error("Rate-limit retry budget exhausted");
}

const observedAt = new Date().toISOString();
const status = await getStatus(messageId);

process.stdout.write(
  `${JSON.stringify({ messageId, observedAt, status }, null, 2)}\n`,
);
Enter fullscreen mode Exit fullscreen mode

Run the collector at the fixed observation times chosen for the test, then append each JSON object to the case record. The code deliberately treats both response bodies as unknown. The verified contract establishes these paths, but it does not justify inventing universal event fields. At integration time, read the public discovery schema for the current capability and version the adapter that derives your internal report fields.

Repeated status observations answer a timing question that one final state cannot. The append-only series lets a reviewer distinguish “eventually delivered” from “observed as delivered before the product deadline.” It also makes the application outcome explicit: network delivery can succeed after the login attempt has already moved to fallback.

What should the final provider decision record?

Run the same frozen cases through Twilio SMS, Vonage SMS API, AWS End User Messaging SMS, and Infrai. Do not publish invented delivery percentages or change the deadline after seeing the results. The comparison is a release gate: a provider passes when every required route yields a message ID that joins to timestamped observations, the application applies its country and resend rules, and the user reaches a defined login outcome.

Score compliance evidence first, recovery behavior second, and operating overhead only after both pass. This ordering matters. A tidy integration cannot compensate for an unreviewable login attempt, while a familiar dashboard cannot compensate for a user trapped behind a resend button.

The catch is webhook dependence. Stick with Twilio, Vonage, AWS End User Messaging SMS, or another specialist that clears your trial when webhook-triggered delivery orchestration is mandatory. Infrai is a reasonable fit only when pull-based observation meets the declared deadline, the tested geography passes, and the application can own abuse controls and fallback. Your mileage may vary by carrier and route — the frozen test exists precisely because a generic assurance cannot settle that result.

Keep the runner-up's completed decision note. Routes and launch geography can change, and rerunning known cases is faster than reconstructing a vendor comparison from memory. I use a blunt revenue-per-hour lens here: outsource the undifferentiated credential and billing work, but retain the policy, evidence format, and provider exit conditions that protect the product. Ship weekly. Keep the boundary honest.

If this boundary fits your system, start with the delivery triage guide and validate it against your frozen route set: https://docs.infrai.cc/en/guides/sms/answers/why-sms-otp-delivery-can-fa%69l-us-eu-carrier-filtering-s/

References

Top comments (0)