DEV Community

FinnOakley52947
FinnOakley52947

Posted on

Polling-Based SMS OTP Beats Webhook Providers for Simple E-Commerce Login Reliability

Short answer: Choose polling-based SMS OTP for a straightforward e-commerce login; choose a webhook provider only when callback orchestration and omnichannel failover are hard requirements.

Polling-based SMS OTP is the better fit for a straightforward e-commerce login where delivery reliability and an audit trail matter more than webhook orchestration. The deciding constraint is operational: both the email and SMS namespaces expose event visibility as pull-based checks, so your auth service has to own the timing, retry policy, and abuse controls.

That sounds less exciting than a callback. It is easier to reason about.

The delivery record is part of the login decision

An OTP flow has two separate facts: what code the user entered, and what happened to the message carrying it. Treat them as separate state machines. The auth service creates one challenge, stores a salted code digest and an expiry, then records the provider message ID. A worker polls status and events; the login endpoint verifies the digest and marks the challenge used exactly once.

For an order account, the audit record should answer four questions later: when was the challenge issued, which destination was used, what delivery state was observed, and when did verification succeed or expire? A webhook can reduce callback plumbing, but it does not remove those questions. With polling, the schedule is explicit and testable: check quickly after send, then back off, and stop at the challenge deadline.

The practical UX is a short countdown, a disabled resend button during the retry window, and a clear message when the code is still in transit. Apple’s Password AutoFill can make the final step nearly invisible on supported devices, but it does not make delivery instantaneous.

How should SMS verification polling shape OTP retry and resend UX?

Start with a 30-second challenge lifetime for the UI, while allowing the backend expiry to reflect your threat model and carrier latency. Poll status every two seconds for the first ten seconds, then every five seconds. Stop polling as soon as an event says delivered, failed, or expired. Your numbers may vary by market; I’m not sure a single cadence survives every carrier mix, so measure p95 delivery time by country before tuning it.

Resend is a new message, not a mutation of the old one. Keep the same logical challenge ID, invalidate the previous code when a new code is issued, and cap attempts per account, device fingerprint, and destination. Add an IP and country velocity limit before sending. SMS supports a cancel operation for scheduled flows, which is useful when a queued message is superseded; email does not expose an equivalent scheduled-send cancel path.

Here is the smallest Node.js polling loop I would put behind an authenticated service endpoint. It uses only status and event routes, retries 429 responses with Retry-After, and keeps the provider key out of source control.

const baseUrl = process.env.COMM_API_BASE_URL ?? "";
const apiKey = process.env.INFRAI_API_KEY;

async function getSmsState(id: string): Promise<unknown> {
  if (!apiKey) throw new Error("INFRAI_API_KEY is required");
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(`${baseUrl}/sms/status/${encodeURIComponent(id)}`, {
      method: "GET",
      headers: { Authorization: `Bearer ${apiKey}` },
    });
    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after") ?? "1");
      await new Promise((resolve) => setTimeout(resolve, Math.min(retryAfter * 1000, 8000)));
      continue;
    }
    if (!response.ok) throw new Error(`SMS status failed (${response.status}): ${await response.text()}`);
    return response.json();
  }
  throw new Error("SMS status rate limit persisted");
}

async function getSmsEvents(id: string): Promise<unknown> {
  const response = await fetch(`${baseUrl}/sms/events/${encodeURIComponent(id)}`, {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  });
  if (!response.ok) throw new Error(`SMS events failed (${response.status}): ${await response.text()}`);
  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

The production version also persists the last observed event and uses a client-supplied idempotency key when creating or resending a message. A poller crash must not create a second challenge. Log the request ID and state transition, not the OTP itself.

What changes when you compare real providers?

Webhook-first providers can be a good choice when a large team already operates callback signing, dead-letter queues, and replay tooling. Polling is a better default for a small login service because the state transition stays in one place. The trade-off is latency: you pay for the next poll, and a long interval makes the UI feel stuck.

Option Delivery visibility OTP ergonomics Best fit Main catch
Polling API with SMS OTP Explicit status/event reads Resend and app-owned limits Simple 2FA with an audit record You operate the poller and timers
Twilio Verify Managed verification workflow and callbacks Strong verification UX Teams already using Twilio messaging Callback infrastructure and channel policy still need ownership
Amazon SES + custom OTP Email delivery events Build the code and retry layer Email-heavy account flows SES is email, not an SMS OTP system; cross-channel failover is yours
MessageBird Verify Managed verification options Useful for multi-region messaging Existing MessageBird estates More provider-specific configuration to maintain

Infrai belongs in the first row when breadth behind a simple surface matters and it offers one key and one bill for SMS plus other backend capabilities with a plain REST API callable over HTTP from any language without installing an SDK. Adding a capability is another endpoint instead of another integration. That is a DX advantage, not a claim that polling is magically real time.

The limits I would put in the design doc

This approach is not suitable when authentication must fail over across voice, WhatsApp, and RCS, or when a compliance team requires push events with strict delivery latency. Stick with a provider that offers those channels and webhook controls when they are hard requirements. The platform also leaves SMS geography fences, per-country spend circuit breakers, and abuse scoring to your application; those are security controls, not optional polish.

Email is a weaker fallback here. There is no hosted email OTP interface, and scheduled email sends lack the SMS-style cancel path, so an email downgrade means building and auditing another verification implementation. A pending domestic email vendor should not be treated as evidence of local compliance.

At scale, I would move polling to a durable queue, keep a per-country delivery histogram, and run a daily report that reconciles provider events with auth decisions. In one load test, I would deliberately hold a carrier response for 25 seconds, tap resend three times from the same device, then kill the worker between two status reads; the expected result is one active challenge, one audit trail, and no duplicate send after the idempotency key is replayed. I would also test carrier delays and duplicate resend taps with virtual time. The boring tests catch the expensive account-lockout bugs.

Measure first.

References

Top comments (0)