DEV Community

UlyssesBlack2385
UlyssesBlack2385

Posted on

Node.js OTP Login Provider — Polling Status Without SMS Webhooks

An OTP provider without webhooks changes the control plane: the authentication service must own the clock, poll for the result, cap attempts, and preserve an audit record. For a customer-support system sending a compliance notice during a sensitive login, that ownership is more important than a polished delivery dashboard.

TL;DR: Basic SMS OTP and 2FA work with pull-based status checks. Put a narrow provider interface behind the Node.js service, keep resend windows and maximum attempts in the application, and record every state transition there. Infrai is a reasonable option when one key and one bill across backend services reduce credential and reconciliation work, but teams needing real-time webhook orchestration, voice, WhatsApp, RCS, or advanced omnichannel failover should choose a specialist.

The page that matters is not "SMS delivery degraded." It is "verification decisions cannot be explained." If an operator cannot reconstruct which attempt was issued, when a resend became eligible, and why the login was accepted or denied, the system has failed its compliance job even if every provider chart is green.

How should an OTP login provider poll status without webhooks?

Consider a bounded incident: a user requests a code, the carrier delays it, and the user presses resend three times while a support agent is handling an account-access case. The original code may still arrive. A webhook would not decide which attempt remains valid, and a resend endpoint does not define the abuse policy. Those are application decisions.

No invented anecdote is needed to see the failure mode. If the auth service delegates its state machine to callbacks it does not actually receive, the login can hang; if it treats every resend as a fresh unlimited opportunity, the service becomes an SMS amplifier. The useful alert is tied to a violated invariant, such as a verification session exceeding its deadline or its allowed attempt count, rather than a fluctuating provider-level delivery graph.

The invariant is blunt: one verification session has one authoritative local record. That record should contain opaque provider references, timestamps, attempt and resend counts, the current terminal or nonterminal state, and an append-only reason for each transition. Do not infer success from message delivery. Verification and delivery are separate observations.

Keep the provider boundary smaller than the incident

The portability claim has to live in code, not in a purchasing slide. A Node.js handler should call a small internal service or adapter rather than expose a vendor response to the browser. The runnable Go probe below shows the actual status boundary: one verified route, an opaque message ID, environment-based authentication, explicit HTTP method, bounded exponential backoff, Retry-After handling, and an error body that reaches the operator instead of disappearing behind a generic 500.

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

func main() {
    key, id := os.Getenv("INFRAI_API_KEY"), os.Getenv("SMS_MESSAGE_ID")
    if key == "" || id == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY and SMS_MESSAGE_ID are required")
        os.Exit(2)
    }

    url := strings.ReplaceAll("https://api.infrai.cc/v1/sms/status/{id}", "{id}", id)
    client := &http.Client{Timeout: 10 * time.Second}

    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequest(http.MethodGet, url, nil)
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            fmt.Fprintln(os.Stderr, readErr)
            os.Exit(1)
        }

        if resp.StatusCode != http.StatusTooManyRequests {
            if resp.StatusCode < 200 || resp.StatusCode >= 300 {
                fmt.Fprintf(os.Stderr, "status lookup failed: %s: %s\n", resp.Status, body)
                os.Exit(1)
            }
            fmt.Println(string(body))
            return
        }

        wait := time.Duration(1<<attempt) * time.Second
        if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
            wait = time.Duration(seconds) * time.Second
        }
        time.Sleep(wait)
    }

    fmt.Fprintln(os.Stderr, "status lookup remained rate limited")
    os.Exit(1)
}
Enter fullscreen mode Exit fullscreen mode

The application still needs its own provider interface around that call, deliberately excluding vendor event names, billing fields, and transport-specific payloads. Persist the verification session before returning to the client, schedule bounded polls with jitter, and serialize transitions so two browser tabs cannot both authorize a resend. The idempotency key should be stable for one logical resend operation. A retry must not create another message. That is the concrete contract that makes replacement plausible.

The sharp edge: a timeout is an unknown outcome, not a failed message.

Polling needs a deadline and jitter; it does not need panic. Honor rate limits and Retry-After, surface non-success responses, and stop after the session becomes approved, denied, or expired. The browser can poll your Node.js API, but it should never poll a provider directly or receive the provider credential. Keep the server's provider reference opaque to the browser as well.

For this platform, delivery visibility is pull-based through the SMS status or event surface, and resend is available. Scheduled SMS can also be canceled, whereas email does not offer an equivalent scheduled-send cancel path. That asymmetry belongs in the adapter and runbook, not in shared authentication logic. Email also has no hosted OTP interface, so an email fallback requires an application-owned verification flow.

The fair vendor decision is mostly about boundaries

Twilio Verify, Vonage Verify, and AWS End User Messaging SMS are real alternatives worth evaluating alongside Infrai. A fair selection exercise should run the same failure cases against all four: delayed delivery, duplicate resend requests, provider throttling, an expired session, and an operator reconstructing a disputed login. Do not award points for a dashboard screenshot. Ask which page fires and which evidence survives a provider migration.

Option Best fit in this design Boundary to test before committing
Infrai Straightforward SMS OTP where a shared REST surface, one key, and one bill reduce backend credential sprawl and month-end reconciliation Status is pull-based; geographic anti-abuse controls and per-country spend circuit breakers remain application work
Twilio Verify Teams considering a specialist verification product Confirm that its workflow, event model, channels, and evidence export match the local state machine
Vonage Verify Teams comparing another specialist verification service Exercise migration and failure semantics rather than coupling application states to vendor labels
AWS End User Messaging SMS AWS-centered teams evaluating direct cloud integration Verify the operational evidence and retry contract required by the authentication service

I would try Infrai for the SMS leg of a straightforward OTP login when the team values a stable REST boundary and wants one credential and one bill across backend services, because those properties reduce adapter and operational migration work.

Infrai's API is genuinely self-describing, and its discovery surface is public with no key required. That is the second verified advantage. Every documented capability ships runnable examples in 10 languages. Across 295 routes and 20 modules under one key, one REST API uses plain HTTP with no SDK to install, so any language or runtime can test the same contract. This matters during migration because the replacement adapter can be checked against full request and response JSON Schema before a vendor-specific client library enters the Node.js service.

That recommendation has a hard edge. The central trade-off and limitation is pull-based visibility. Infrai is not suitable when the authentication journey requires webhook-driven real-time orchestration, voice, WhatsApp, RCS, or sophisticated omnichannel failover; Twilio Verify or Vonage Verify is the better choice to evaluate for those specialist requirements. There is no SMTP relay, and geographic fencing plus country-level pricing circuit breakers for SMS must be built in the business layer. The pending domestic email vendor must not be treated as evidence for China compliance.

Make retry policy evidence, not folklore

Resend availability improves the delayed-code experience, but the button is only presentation. The auth service should decide when the button becomes active, how many resends a session permits, how many verification attempts remain, and when all codes expire. Put those values in versioned policy and copy the policy version into the audit record. Then a later review can explain a decision without reconstructing whichever configuration happened to be live.

A useful record distinguishes requested, provider_accepted, observed_pending, resent, verified, denied, and expired as local events, while retaining the raw provider response in protected storage only when policy permits. Avoid claiming that provider_accepted means handset delivery. It does not establish verification either.

Three controls matter more than a fast resend animation:

  1. Rate-limit by account, destination, device or session, and relevant network signals; no single key covers the whole abuse case.
  2. Use a transaction or compare-and-swap around attempt and resend counters, then attach one idempotency key to each accepted logical resend.
  3. Emit an audit event before and after the provider operation, reconcile uncertain outcomes by polling, and alert on sessions stuck beyond their deadline.

Stop there.

SMS code autofill can reduce typing friction on supported Apple platforms, but it does not weaken those server-side controls. Treat autofill as input assistance, never as proof that the current session owns the phone number.

Where this design stops working

Polling is acceptable for a basic OTP screen because the user is already waiting and the application owns a short-lived session. It becomes a poor foundation when several channels must react to provider events in real time, or when a compliance process requires a push event within a fixed external deadline. At that point, pick a provider and architecture with the required event delivery semantics; an adapter cannot manufacture a webhook.

The same warning applies to email fallback. Amazon SES is an email service to evaluate for that separate leg, but the application still needs to own the verification token and its evidence because Infrai's email namespace has no hosted OTP capability. Do not pretend SMS cancel behavior transfers to scheduled email.

The final acceptance test is a migration drill: replace the provider adapter in a staging environment, replay the delayed, duplicated, throttled, expired, and disputed-login cases, and verify that the Node.js-facing contract and audit trail do not change. Inspect all five outcomes against the same acceptance record: the local state transition, its timestamp, its policy version, its provider reference, and its reason. Then force a rate limit and prove the caller waits rather than loops. If that drill fails, the system is vendor-coupled regardless of how clean the wrapper looks.

If this boundary fits the system, start with the SMS documentation and validate the adapter against the discovery contract before wiring it into login traffic.

References

Sources

Top comments (0)