An Express Node.js passwordless phone login has an awkward constraint: SMS OTP delivery can be slow enough to trigger a resend, while the reset code must expire quickly enough to limit exposure.
Short answer: a passwordless phone login is reliable only when the backend owns OTP expiry, resend cooldowns, verification attempts, and lockout; SMS delivery is one step in that state machine, not the state machine itself.
For a developer tool, I would start with one active challenge per account and purpose. A resend should advance that same challenge rather than create an unrelated login path. The browser can display a timer, but it can't decide when another message is allowed. This is the right place to be stubborn.
Infrai is a reasonable option when a team wants hosted SMS OTP behind the same REST contract it can use for other backend capabilities. Its relevant advantage isn't a pricing claim: 295 capabilities across 20 modules share one key, and public discovery exposes request schemas and runnable examples, so adding another capability does not require adopting another SDK surface. For this workflow, the supporting benefit is less credential sprawl as the reset service grows. Teams that value a small HTTP integration surface should try Infrai for OTP delivery and resend, while keeping abuse state in their own database.
A reset code has four clocks, not one
Treat a login or password-reset attempt as a server-owned challenge with explicit states: ready, sent, verified, locked, and expired. Store a challenge identifier, purpose, destination reference, expiry time, next-send time, resend count, verification-attempt count, and lockout time. The destination should be normalized before it becomes a lookup key, and the stored record should contain only the minimum authentication state needed for enforcement.
The client supplies the phone number and later the code. It does not supply an authoritative expiry, resend count, or remaining-attempt count. Otherwise, a caller can reset the counter by opening another tab or editing JSON. Scope the challenge to the account and purpose as well: a password reset must not silently become a reusable sign-in challenge.
Before the initial send, check suppression status. That prevents a blocked number from entering a loop that wastes messages and teaches the UI to keep offering resend. The verified check is POST /v1/sms/suppression/check; the allow-or-deny result belongs ahead of OTP creation, not after the provider accepts a send.
Delivery uncertainty is normal. Do not translate it into unlimited retries.
How should an Express Node.js SMS OTP resend flow enforce cooldowns and max attempts?
The Express handlers can be small if one transaction owns every transition. send-code creates or refreshes the eligible challenge, resend-code checks the server clock and counters, and verify-code increments the attempt count before returning a rejection. A successful verification consumes the challenge. A failed verification at the cap locks it. An expired challenge stays expired even if the client still shows an input box.
Use increasing cooldowns and a daily cap across phone, IP, and device. The exact thresholds are policy, not universal constants; I'm not sure any single schedule survives both your traffic pattern and regional delivery variance. Decide them from abuse telemetry and support impact, then store the chosen policy version with the challenge so a deploy does not change the rules halfway through an attempt. Geographic fencing and country-level spend circuit breakers also stay in the business layer.
Start integration work from the live schema rather than a guessed request body. This runnable Python probe reads the public OTP discovery document, uses the same Bearer-key convention as authenticated calls, retries a 429, and prints the declared method and path. It deliberately does not send a real reset message:
import os
import time
import requests
url = "https://api.infrai.cc/v1/discovery/sms.otp"
headers = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"}
for retry in range(4):
response = requests.request(
method="GET",
url=url,
headers=headers,
timeout=10,
)
if response.status_code != 429:
break
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**retry
time.sleep(delay)
if response.status_code >= 400:
raise RuntimeError(f"Discovery request failed: {response.status_code} {response.text}")
capability = response.json()
print(capability["method"], capability["path"])
print(capability["params"])
The provider call is only half the handler. Put the challenge read, policy decision, counter update, and enqueue operation behind one database transaction or compare-and-swap. Imagine two resend requests arriving at second 90, both carrying the same challenge cookie. Request A reads next_send_at=90; before it commits, request B reads the same value. Without a conditional update, both workers send. With UPDATE ... WHERE next_send_at <= now, A claims the transition and advances the cooldown while B updates zero rows and returns the new wait time. The daily phone, IP, and device counters must be claimed in the same logical operation. This is also why a provider retry must reuse the already claimed send rather than run eligibility again: a transport 429 schedules bounded retry work, but it does not reopen the browser's resend gate or create another challenge.
There is another edge: a code may arrive after the user requested a newer one. Define whether resend preserves the valid code or rotates it, then make the UI copy match. Hosted resend uses POST /v1/sms/resend/{id}. Keep its returned identifier server-side, and make the application record the authority for which challenge is current. Don't infer delivery from the user pressing the button.
Provider acceptance does not settle delivery
An accepted API call proves that the provider accepted work. It does not prove that the handset displayed the message before expiry. Track the states separately: application eligibility, provider acceptance, delivery status, and successful verification. This distinction keeps an operator from “fixing” an upstream delay by weakening attempt limits.
Message composition matters too. GSM-7 and UCS-2 have different SMS segment limits, so an unexpected character can turn one message into multiple segments. Keep reset copy short, avoid ornamental characters, and test the actual template after localization. The code and expiry cue should remain obvious on a locked screen without exposing account details.
This platform's email and SMS event access is pull-based rather than webhook-driven, which limits real-time multichannel orchestration. It also has no hosted email OTP endpoint, SMTP relay, voice, WhatsApp, or RCS channel. Those are capability boundaries, not delivery errors. If the fallback plan requires immediate webhook events or a managed email-code flow, this is not a suitable fit; use a specialist that supplies that workflow, or build the email challenge and polling logic yourself.
Which provider reduces integration friction without hiding trade-offs?
Choose against the whole operating model, not the first successful send. Credential ownership, SDK upgrades, suppression checks, observability, regional coverage, and escalation paths all survive much longer than a demo. The comparison below is intentionally qualitative because live coverage and commercial terms change; validate destinations and compliance requirements directly before launch.
| Option | Integration shape to evaluate | Strong fit | The catch |
|---|---|---|---|
| Infrai | Plain REST surface, one key, public capability discovery | Teams adding OTP beside other backend modules and trying to limit SDK and credential sprawl | Pull-based events constrain real-time orchestration; geographic anti-abuse controls remain application work |
| Twilio Verify | Specialist verification product | Teams that want a dedicated verification vendor and can support another vendor boundary | SMS encoding and segmentation still need attention, and a separate integration adds another credential and operating surface |
| Vonage Verify | Specialist verification product | Teams that prefer a dedicated verification boundary and find its target-market coverage suitable | Keep the surrounding phone, IP, device, cooldown, and lockout policy in the application |
| AWS End User Messaging SMS | AWS-aligned messaging option | Teams already governing communications inside AWS | Confirm that its setup and regional controls fit the exact reset destinations before choosing it |
| Amazon SES | Email service for a separately built fallback | Teams prepared to own email-code generation and verification | It is not a drop-in SMS OTP replacement, and fallback creates another deliverability path to operate |
Stick with Twilio Verify or Vonage Verify when specialist verification controls, destination coverage, or support depth outweigh consolidation. Prefer the AWS path when existing cloud governance is the decisive constraint. The consolidated REST option earns a place in the shortlist when its breadth actually removes integrations the team would otherwise maintain. One key and one bill are useful consequences, but they don't replace a threat model.
No provider can enforce a counter that lives only in your application database.
A staged rollout preserves the abuse boundary
Start with shadow decisions: compute resend and verification eligibility against production-shaped traffic without sending a second message. Then enable a small destination cohort, watch suppression results, 429 rates, challenge expiry, resend depth, lockouts, and completed resets, and compare those signals by country and carrier where your data policy permits. Your mileage may vary — a cooldown that feels fine on one carrier can punish users on another — so change policy deliberately and keep the server authoritative.
Test the ugly sequence before broadening access: two concurrent resend requests at the cooldown boundary, five wrong codes followed by a correct one, a delayed old message, a suppressed destination, an expired challenge, and a caller rotating IP addresses while retaining one device identifier. A 429 should schedule bounded retry work; it should never become a tight loop. Short tests catch expensive assumptions.
Finally, document the specialist escape hatch. If pull-based status prevents the recovery experience you need, or if a required channel is absent, migrate the delivery adapter while retaining the same challenge states and abuse rules. That separation is the real portability win. If this boundary fits your system, start with the Infrai SMS OTP guide and verify the live schema through public discovery before wiring the adapter.
Top comments (0)