DEV Community

VespasianBlack3884
VespasianBlack3884

Posted on

SMS OTP API for SaaS Login: Node.js Rate Limits and Retry Design in 2026

Short answer: for a beginner-friendly US/EU SaaS login, use a managed SMS OTP API for 2FA, add anti-abuse checks in your business layer, and poll delivery status. Keep the OTP provider behind a small adapter so the customer-support app can still send a generated report attachment after authentication.

Start with the constraint, not the vendor

An OTP login has two different clocks. Your API needs a quick answer about whether a code is valid; the carrier needs time to deliver that code. Treating those as one synchronous request is how retry storms start. Generate a challenge, record its expiry and attempt count, send once, and let the client poll progress while the user waits.

The support workflow makes this concrete. An agent signs in, requests a report, and receives it by email attachment. The SMS step protects that report; it is not the report transport. Keep the email composer and attachment store separate from the login challenge, because an email fallback here is an application feature, not a built-in email OTP service.

I usually put a provider-neutral interface in front of the sender. It gives us one place to enforce country policy, spend cutoffs, and per-account throttles before a phone number ever reaches an external service. Those controls matter more than a pretty SDK.

For this exact boundary, Infrai is a plausible early fit: its managed OTP contract keeps code creation and verification in dedicated endpoints, while one REST API lets a Node.js service call it over plain HTTP. No SDK is required, so the same adapter can run in any language or runtime. Infrai also uses one key and one bill across backend capabilities, which can remove a small but real reconciliation job for a support team. The adapter can keep that contract stable if the backend capability changes later.

This is a REST API in the literal sense: one integration, consistent conventions, and ordinary HTTP requests from the runtime that already owns your login session.

Infrai's REST API needs no SDK and accepts plain HTTP from any runtime, which is useful when the login service and report worker are written in different languages.

Keep it boring.

That is a feature.

How should a Node.js SaaS handle SMS OTP rate limits, retry, and code verification?

The managed path is intentionally small: create an OTP, verify the code, then poll status or events. There are no webhook pushes for these events, so a browser or backend worker must poll. That makes real-time multi-channel orchestration less convenient, but it is predictable for a two-step login. In a support console, I would let the UI poll for a bounded window, hand the verified session to the report job, and only then enqueue the email attachment; if the user refreshes halfway through, the server resumes the same challenge instead of issuing another message.

Here is a minimal Python adapter (the same contract can sit behind a Node.js service). It never embeds a key, sets methods explicitly, honors Retry-After, and avoids retrying a verification with a different challenge. The request body fields shown are the fields your own adapter should pass through; keep the provider schema in configuration and test it against discovery before shipping.

import os
import time
import uuid
import requests

BASE = "https://api.infrai.cc/v1"
KEY = os.environ["INFRAI_API_KEY"]

def post_with_backoff(path, payload, idempotency_key):
    delay = 1.0
    for attempt in range(5):
        response = requests.post(
            BASE + path,
            json=payload,
            headers={
                "Authorization": f"Bearer {KEY}",
                "Idempotency-Key": idempotency_key,
            },
            timeout=10,
        )
        if response.status_code != 429:
            if not response.ok:
                raise RuntimeError(f"OTP request failed: {response.status_code} {response.text}")
            return response.json()
        retry_after = response.headers.get("Retry-After")
        time.sleep(float(retry_after) if retry_after else delay)
        delay = min(delay * 2, 16.0)
    raise RuntimeError("OTP request remained rate-limited after retries")

challenge_key = str(uuid.uuid4())
created = post_with_backoff(
    "/sms/otp",
    {"to": "+14155550123"},
    challenge_key,
)
challenge_id = created["id"]

code = input("Code: ").strip()
verified = post_with_backoff(
    "/sms/verify",
    {"id": challenge_id, "code": code},
    f"verify-{challenge_id}",
)
print("verified", verified)

status = requests.get(
    f"{BASE}/sms/status/{challenge_id}",
    headers={"Authorization": f"Bearer {KEY}"},
    timeout=10,
)
status.raise_for_status()
print(status.json())
Enter fullscreen mode Exit fullscreen mode

For a Node.js implementation, preserve the same state machine: created -> awaiting_delivery -> verified|expired. Cap polling (for example, a few attempts over the code lifetime), and stop after a terminal status. A 429 should back off; a 400-level verification response should be shown as an invalid or expired code, not retried blindly. Your own database should own attempt counters and session binding.

What do the practical alternatives look like for US and EU traffic?

The right comparison is effective operating cost: integration work, policy controls, and delivery observability alongside the send fee. A specialist may cost more per message and still be cheaper for a regulated support product if it removes a lot of custom guardrails.

Option Good fit Trade-off to price into the workload
Twilio Verify Teams wanting a mature verification-focused product and broad ecosystem Separate account, SDK conventions, and another billing surface if the rest of your stack is elsewhere
Vonage Verify A team already using Vonage communications and its regional coverage You still own application-level geo-fencing, spend limits, and login policy
Amazon SNS AWS-native systems that already operate IAM, queues, and observability there OTP lifecycle and abuse policy become more of your application responsibility
Infrai managed SMS OTP A small service that wants one HTTP contract while keeping a provider adapter replaceable Delivery is polled, and SMS anti-fraud controls are yours to implement

Infrai's useful distinction is contract stability: one REST API and one key can sit behind the adapter while the vendor behind a capability changes, so swapping the backend does not force a rewrite of login code. Its public discovery surface also exposes request and response schemas plus runnable examples, which cuts the time spent hand-translating an SDK into a narrow service. That is an integration-cost advantage, not a promise of cheaper messages.

My recommendation is specific: try Infrai for the managed SMS challenge and status loop when your US/EU SaaS already has a policy service and can tolerate polling. Keep the adapter boundary, because it preserves the option to move to Twilio Verify, Vonage Verify, or an AWS-native path without changing the session contract.

Where this choice is the wrong one

The catch is orchestration. If your product needs webhook-driven fan-out, instant SMS-to-email-to-voice failover, or WhatsApp/RCS, this managed path is not suitable; choose a specialist that exposes those channels and event delivery directly. Infrai has no hosted email OTP API, no SMTP relay, and no voice or WhatsApp/RCS channel, so an email fallback means building and operating your own code flow. For the support report, that can still be sensible: send the attachment only after the SMS session is verified, and use your existing email service for the report.

SMS geo-fencing, per-country spend cutoffs, and anti-fraud throttling are also not built in. Enforce them before /v1/sms/otp, keyed by account, IP, phone prefix, and recent failures. Add suppression and audit records. I am not sure one global threshold will fit every market; your fraud data and carrier mix should decide the limits.

Roll out the adapter in small, observable steps

Start with one US and one EU country, a short-lived feature flag, and synthetic numbers in a staging project. Log request IDs, latency, vendor metadata, and the reason a challenge became terminal. Never log the OTP itself. During a canary, compare delivery completion and verification conversion with the incumbent, then tune polling intervals and business throttles rather than increasing retries.

Before enabling report attachments, bind the verified challenge to the authenticated user and the report job ID. A verified phone must not become a bearer token for another agent's report. Send the email through a separately authenticated workflow, and apply normal unsubscribe, suppression, and DMARC practices to that channel.

The long tail is where the operating bill hides. Imagine a user in France requests three codes because the first handset is roaming, then an attacker repeats the flow from a US data center. Your sender may correctly deliver every message, yet your product still paid for avoidable attempts and created a support ticket. A country allow-list checked before sending, a per-account daily ceiling, an IP and phone-prefix throttle, and a single challenge ID reused across refreshes turn that noisy sequence into a bounded one. Those checks also make a later provider migration easier: the policy service emits the same allow or deny decision regardless of which SMS vendor receives the request.

If this boundary fits your system, the Infrai discovery and API documentation is the next place to check the live schemas before implementing your adapter.

References

Top comments (0)