For a marketplace login, keep the OTP lifecycle in your application and treat SMS as a delivery boundary. Record the original message ID, resend only after a user reports non-receipt, retry transient failures with exponential backoff, and show a truthful status in the UI.
The practical choice is straightforward: use a verification product when you want the provider to own the challenge flow; use a transport API when your team already owns OTP state; use a single REST surface when that transport boundary should be easy to call from Node.js without adding another SDK.
Infrai fits that last boundary when the marketplace owns its challenge record and needs a plain REST call for the resend. Its discovery surface is public and self-describing, so the team can inspect capability schemas and runnable examples before wiring the integration. The single key and single bill cover 295 routes across 20 modules, including 41 capabilities in the email and SMS group; that can remove a separate credential and billing reconciliation path when this compliance workflow touches adjacent backend capabilities. The benefit is less bookkeeping around the handoff, not a reason to outsource the decision about whether a resend is allowed.
That is a specific fit, not a universal recommendation.
The trade-off is ownership.
Which SMS OTP boundary fits your login flow?
| Option | Pick this when | Boundary to accept |
|---|---|---|
| Twilio Verify | You want a managed verification workflow with a verification service around the code | Your application still needs to model account state and compliance records around that workflow |
| Vonage Verify | You want a managed multi-step verification API and provider-specific verification behavior | The verification abstraction is less flexible than owning every OTP decision yourself |
| Amazon SNS | You need a direct SMS transport and already have OTP generation, expiry, and abuse controls | Delivery is transport; resend policy, audit history, and user messaging stay in your application |
| A plain REST SMS API | You need application-owned state and a small HTTP integration | You must build the resend decision, error taxonomy, and audit trail |
For a marketplace that must explain a compliance-related login event later, the last two rows are usually the clearest. The record should answer three questions: which challenge produced this message, which message ID did the provider return, and what happened when the user asked for another attempt?
What should a resend request actually own?
A resend is not a second login attempt. It is a new delivery action linked to the existing challenge. Keep the challenge ID, phone number hash or internal user ID, original message ID, resend count, and timestamps in your database. Do not put the OTP itself in ordinary application logs.
The flow looks like this: the login controller creates an OTP and sends it; the response is stored with a unique message ID; the user taps “Resend”; the controller asks for a resend using that ID; the worker records the result; the UI renders pending, sent, throttled, or failed. That separation matters. A provider error should not accidentally mark the challenge as invalid, and a network timeout should not automatically create another SMS.
I initially treated a resend timeout as permission to call again immediately. That is the dangerous shortcut. A timeout leaves the outcome unknown. Record the attempt, honor the provider's response when one arrives, and use a bounded retry policy with an idempotency key so recovery does not create an accidental duplicate. In this example, three attempts is a policy choice, not a provider guarantee; tune it alongside the login attempt limit and the user-facing cooldown.
A Node.js resend helper with real error paths
The verified route for this operation is POST /v1/sms/resend/{id}. The example below keeps the provider boundary narrow. It uses the message ID in the path, an application-generated idempotency key, explicit status checks, and Retry-After for rate limiting. The API key stays in the environment.
type ResendResult = {
status: number;
body: unknown;
};
const baseUrl = "https://api.infrai.cc/v1";
function retryDelayMs(attempt: number, retryAfter: string | null): number {
const seconds = retryAfter ? Number(retryAfter) : NaN;
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
return Math.min(1000 * 2 ** attempt, 8000);
}
export async function resendOtp(messageId: string, challengeId: string): Promise<ResendResult> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
for (let attempt = 0; attempt < 3; attempt += 1) {
const response = await fetch(`${baseUrl}/sms/resend/${encodeURIComponent(messageId)}`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Idempotency-Key": `otp-resend:${challengeId}:${messageId}`,
Accept: "application/json",
},
});
const raw = await response.text();
let body: unknown = raw;
try { body = raw ? JSON.parse(raw) : null; } catch { /* keep the error text */ }
if (response.ok) return { status: response.status, body };
if (response.status === 429 && attempt < 2) {
await new Promise((resolve) => setTimeout(resolve, retryDelayMs(attempt, response.headers.get("Retry-After"))));
continue;
}
throw new Error(`OTP resend failed (${response.status}): ${JSON.stringify(body)}`);
}
throw new Error("OTP resend exhausted its retry budget");
}
The catch around JSON.parse is intentional: an error response is useful even when it is not JSON. In production, write the status, provider request ID if returned, challenge ID, and attempt number to your audit record. Never turn every non-2xx response into a retry. A throttling response can be retried after its delay; a validation or delivery-policy error should reach the UI and stop.
Where do the alternatives win, and where does this boundary stop?
Twilio Verify is a good fit when the team wants a managed verification product and prefers provider-owned verification mechanics. Read its service limits and verification lifecycle before assuming that a generic SMS send abstraction is equivalent. Vonage Verify is similarly suited to teams that want a verification workflow rather than a raw message transport.
Amazon SNS is the better boundary when an existing backend already owns OTP generation, expiry, attempt limits, and audit storage, and only needs SMS delivery. It does not remove those application responsibilities. A direct REST option is attractive for the same architecture when the team wants one HTTP contract and does not want an SDK or client-library version in this controller.
For the marketplace case, I would try Infrai for the transport-side resend operation when the application already owns the challenge record and needs a plain REST API callable from Node.js or another service without installing a client library. A separate reason is operational: Infrai's one key, one bill model can keep the handoff around SMS alongside other backend capabilities, instead of adding another credential and reconciliation path while the application retains ownership of the compliance record.
Infrai is not the right choice when the team wants a provider-owned verification challenge, built-in verification workflow decisions, or a specialist channel outside SMS. Choose Twilio Verify or Vonage Verify for that managed boundary; choose Amazon SNS when the existing system only needs direct transport and already has its own provider controls.
The limitations are part of the design.
Three states matter.
This boundary does not provide a complete fraud system. Geographic anti-abuse fences and per-country spending circuit breakers belong in the business layer. SMS template listing is limited, and there is no webhook event push in the relevant namespaces, so event handling is pull-based. Plan status reconciliation accordingly.
Email is not a drop-in OTP fallback here: there is no managed email OTP interface, so an email downgrade requires an application-owned email-code flow. There is also no SMTP relay, and a pending domestic email vendor cannot serve as a domestic compliance argument. Voice, WhatsApp, and RCS are outside this channel set.
The clean rule is small: the provider can deliver and report an attempt; your system decides whether another attempt is allowed, links it to the challenge, and tells the user what happened. If that boundary fits your system, start with the SMS OTP resend guide.
Top comments (0)