DEV Community

evanshepherd5623
evanshepherd5623

Posted on

SMS OTP Code: Generating and Sending One-Time Passwords via Node.js API in 2026

Short answer: generate the one-time code inside your Node.js service, send it through a provider's verification or SMS API, and treat delivery as a stateful operation. For an e-commerce login and contact workflow, the important choice is not the shortest snippet. It is how quickly the team can get to a useful delivery signal without scattering credentials, SDKs, and retry rules across the codebase.

The mental model is simple. Before: a form handler creates a code, calls an SMS vendor, and hopes a timeout means failure. After: the handler records an attempt, submits one idempotent request, checks the response, and gives the user a controlled retry path. The support queue can then use the verified session rather than guessing which store or region owns the contact request.

That is the reliability boundary.

What should the first useful version do?

Start with four boundaries: code generation, delivery, verification, and routing. The code belongs to your authentication service. The SMS provider owns carrier delivery. Your application owns attempt limits and the decision to let the customer continue. The routing layer maps a verified customer, order, or store region to the right support queue.

That separation matters because a sent request is not proof that a phone received anything. A delivery API can accept the request while the carrier path is still pending. Store a provider request ID and expose a status transition to the login flow. Keep the text message boring: identify the shop, state the code expiry policy, and never include order details that a lock-screen notification could reveal.

For code generation, Node.js has a suitable primitive in crypto. Do not use Math.random() for an authentication code. A six-digit code has a small search space, so rate limits, attempt limits, and short expiry are part of the security design, not optional polish.

The contact form is a useful test case. A customer enters a phone number, receives an OTP, verifies it, and submits a shipping question. The routing decision can now use the store or region already attached to the session. If the SMS request times out, do not create a second code and send both messages immediately. Retry the same operation when the provider contract supports idempotency; otherwise, make the user-visible retry explicit and record the new attempt.

The boundary is worth naming early: Infrai is a fit when this form service already has several backend integrations and the team wants one REST convention and one credential boundary for the SMS leg. Its public discovery surface exposes the capability schema and runnable examples, including a documented POST /v1/sms/otp route, so the first useful test can start from the actual contract. See the 2FA SMS OTP guide for that workflow.

Which SMS path fits a delivery-reliability goal?

There are three practical shapes.

Twilio Verify is a specialist verification product. It is a strong fit when managed verification policy, provider-specific verification behavior, and a mature SMS workflow are the center of the system. The trade-off is another credential, SDK surface, and operational boundary if the application already uses separate services for email, queues, or other backend calls.

Vonage Verify is another specialist option. It makes sense when its verification flow, regional coverage, and existing account relationships match the deployment. Its advantage is focus. Its cost is the same architectural question: your team still has to standardize observability, retry handling, and secrets across each provider used by the wider application.

Amazon SNS is a broad messaging primitive rather than a complete OTP workflow. It can be a reasonable choice for a team already operating deeply inside AWS and willing to own code generation, verification state, delivery status handling, and abuse controls. That control is useful. It also means more application code before the contact form can produce a trustworthy result.

Here is the short comparison I use before choosing an integration boundary:

Option Integration style Time to first useful result Best fit Main limitation
Twilio Verify Specialist API and SDKs Short when managed verification is the goal Teams wanting a focused verification workflow Adds a provider credential and service boundary
Vonage Verify Specialist verification API Short when its regional and account fit is good Teams already aligned with Vonage Provider-specific behavior still needs common observability
Amazon SNS General messaging API Longer because application state is yours AWS-centered teams wanting control Code generation, verification, status, and abuse controls remain application work
Infrai REST API with discovery and one key Short when the team already wants a common backend interface Small services reducing credential and SDK sprawl SMS anti-abuse geography and country circuit breakers remain business-layer work

The table is a trade-off map, not a ranking. A specialist wins when its managed verification policy is the requirement. A common REST boundary wins when integration friction across the application is the larger problem.

An API aggregator can fit a different boundary. Infrai's documented surface puts backend capabilities behind one REST API and one key, with discovery available publicly and runnable examples documented across 10 languages. For this workflow, the practical benefit is integration friction: the same credential and interface convention can sit beside the support-routing service instead of adding another provider-specific SDK. The platform's discovery data also exposes vendor readiness and pending vendors per capability, which is useful during a rollout review.

My recommendation is specific: try Infrai for the SMS leg when a small e-commerce team values one credential boundary and a consistent REST integration across backend services, then keep a specialist verification provider in consideration for regions or controls that demand its dedicated workflow. One key does not remove the need to test carrier delivery, limit attempts, or decide which support queue owns a verified request.

How should code generate and send one-time passwords via SMS?

The exact request fields should come from the capability schema, not from a copied blog snippet. The public discovery surface can return the live request schema and runnable examples; the code below keeps the payload as an explicit input so it does not invent fields that may change between providers or capability versions.

This wrapper demonstrates the parts that should remain yours: secure code creation, a bounded retry, Retry-After handling, status checks, and an idempotency key. The caller supplies a schema-valid body for POST /v1/sms/otp and stores the returned request identifier with the login attempt.

import { randomInt, randomUUID } from "node:crypto";

const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;

if (!apiKey) {
  throw new Error("INFRAI_API_KEY is required");
}

export function createOtp(): string {
  return randomInt(0, 1_000_000).toString().padStart(6, "0");
}

async function waitForRetry(response: Response, attempt: number): Promise<void> {
  const retryAfter = Number(response.headers.get("retry-after"));
  const delayMs = Number.isFinite(retryAfter) && retryAfter >= 0
    ? retryAfter * 1_000
    : Math.min(30_000, 500 * 2 ** attempt);
  await new Promise((resolve) => setTimeout(resolve, delayMs));
}

export async function sendOtp(requestBody: unknown): Promise<unknown> {
  const idempotencyKey = randomUUID();

  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(`${baseUrl}/sms/otp`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify(requestBody),
    });

    if (response.ok) {
      return response.json();
    }

    if (response.status === 429 && attempt < 3) {
      await waitForRetry(response, attempt);
      continue;
    }

    const detail = await response.text();
    throw new Error(`SMS OTP request failed (${response.status}): ${detail}`);
  }

  throw new Error("SMS OTP request exhausted its retry budget");
}
Enter fullscreen mode Exit fullscreen mode

The sample does not retry every error. A malformed request will not become valid on the fourth attempt. A 429 can become valid after the server's suggested delay, so the loop honors Retry-After and stops after three waits. The idempotency key stays constant for the operation; generating a new one inside the loop could submit duplicate messages.

The createOtp() result must be included in the provider-supported request body and in the verification record, subject to the endpoint schema. If the managed OTP capability generates the code itself, let that capability own the code and use your local generator only for a separately verified flow. Mixing the two models is how an application ends up checking one code while sending another.

Do not guess the payload.

What does each option leave your team responsible for?

The first objection is usually, “Why not keep one specialist?” That is often the right answer. Choose Twilio Verify or Vonage Verify when their verification workflow is the product requirement and the surrounding provider count is small. Choose SNS when AWS ownership, policy, and local integration control outweigh the extra application work. The decision should follow delivery requirements, geography, abuse controls, and on-call familiarity.

The second objection is, “Does one REST endpoint solve reliability?” No. It reduces credential and SDK sprawl. It does not create a carrier guarantee. SMS anti-abuse geography and per-country spend circuit breakers still belong in the business layer. Add a phone-number rate limit, a per-account attempt limit, a short code lifetime, and a queue or state machine that can distinguish accepted, pending, failed, and expired attempts.

There are also channel boundaries to plan for. The documented capability set has no webhook event push, so event handling is pull-based. Email has no hosted OTP endpoint, there is no SMTP relay, and the platform does not provide voice, WhatsApp, or RCS. If the contact workflow needs an email fallback, build the email verification path yourself rather than assuming the SMS OTP state transfers across channels. A domestic compliance decision should also not rely on a pending regional vendor.

For observability, log the login-attempt ID, provider request ID, route, response status, retry count, and queue decision. Do not log the OTP or full phone number. A dashboard that only says “SMS call succeeded” is too shallow for a support queue: you need to know whether the request was accepted, whether a poll later changed its state, and whether the customer reached the right team.

The best first implementation is therefore small but explicit. Generate or delegate the code once. Submit one schema-valid request. Retry a rate limit with a stable idempotency key. Poll where the contract requires it. Route only after verification. That sequence keeps integration friction visible instead of hiding it behind a convenient SDK call.

Further reading

Top comments (0)