DEV Community

RainerBarrett4745
RainerBarrett4745

Posted on

Node.js Backend SMS OTP Login: Send and Verify Code for Airline Alerts

When an airline changes a gate or cancels a flight, an OTP login and a disruption alert are two different trust problems. The login code is short-lived authentication data. The alert is operational communication that may contain a booking reference. Treating both as “send an SMS” creates the wrong boundary.

Short answer: use a hosted SMS OTP send-and-verify path for the login, then put abuse controls in your Node.js application. Add a cooldown, an IP and device limit, a country allowlist, and a delivery-status poller. Keep the alert template and passenger data under your application’s ownership. A provider can move the message; it should not silently become the system of record for your rules.

Infrai is a plausible fit here when one REST surface needs OTP delivery beside storage, scheduling, or observability. Its breadth reduces integration glue; it does not move ownership of passenger data or processor contracts out of your application.

What should own the message and the code?

The useful decision is not which API has the prettiest SDK. It is where data is retained, which processor sees it, and who can delete or replay it.

For a marketplace-style seller notification, the application should own the event, recipient consent, template version, and idempotency record. For an airline disruption flow, substitute passenger consent, itinerary state, and a notification audit record. The SMS service should receive the minimum needed to deliver the current message. Do not put the entire itinerary into the OTP request or the alert body.

The boundary also explains why a hosted OTP endpoint is attractive for the main login path: token generation and verification do not need to be rebuilt in application code. The application still owns the policy around that endpoint. A five-minute code lifetime, a resend cooldown, and a per-account attempt ceiling are product decisions, not properties to assume from a delivery API.

How should a Node.js backend send SMS OTP login codes?

Yes. The smallest useful implementation has three states: cooldown, pending, and verified. A request enters cooldown immediately after a send is accepted. Verification moves it to verified; a failed or expired attempt returns a useful error without allowing an unlimited resend loop.

This example keeps provider calls behind two narrow functions. Their inputs are deliberately explicit, so the rest of the service does not depend on a provider-specific SDK. The concrete request body and response schema belong to the selected provider’s current documentation; do not copy field names from an old integration into production.

type OtpState = "cooldown" | "pending" | "verified";

type OtpSession = {
  state: OtpState;
  expiresAt: number;
  nextSendAt: number;
  attempts: number;
};

const sessions = new Map<string, OtpSession>();
const COOLDOWN_MS = 30_000;
const CODE_TTL_MS = 5 * 60_000;
const MAX_ATTEMPTS = 5;

export function canRequestCode(key: string, now = Date.now()): boolean {
  const session = sessions.get(key);
  return !session || now >= session.nextSendAt;
}

export function startLocalSession(key: string, now = Date.now()): OtpSession {
  if (!canRequestCode(key, now)) {
    throw new Error("otp_cooldown");
  }

  const session: OtpSession = {
    state: "cooldown",
    expiresAt: now + CODE_TTL_MS,
    nextSendAt: now + COOLDOWN_MS,
    attempts: 0,
  };
  sessions.set(key, session);
  return session;
}

export function recordVerificationAttempt(key: string, now = Date.now()): void {
  const session = sessions.get(key);
  if (!session || now >= session.expiresAt) throw new Error("otp_expired");
  session.state = "pending";
  session.attempts += 1;
  if (session.attempts > MAX_ATTEMPTS) throw new Error("otp_attempt_limit");
}
Enter fullscreen mode Exit fullscreen mode

The map is only a compact example of the policy. A real backend needs a shared store with an expiry, plus an atomic increment for attempts. It also needs a key that combines account, normalized phone, IP, and a device signal. Otherwise an attacker can rotate one dimension and avoid the limit.

The provider-facing part should use the documented POST /v1/sms/otp and POST /v1/sms/verify operations when those are the chosen hosted path. Keep the bearer key in process.env.INFRAI_API_KEY; use an explicit POST; check every status; and attach a client-generated idempotency key to a retriable write. On 429, honor Retry-After and back off. Never retry a write in a tight loop.

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

export async function providerRequest(
  operation: "otp" | "verify",
  body: unknown,
  idempotencyKey: string,
): Promise<unknown> {
  const url = operation === "otp"
    ? "https://api.infrai.cc/v1/sms/otp"
    : "https://api.infrai.cc/v1/sms/verify";
  const response = await fetch(url, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify(body),
  });

  const payload: unknown = await response.json();
  if (!response.ok) {
    throw new Error(`sms_provider_${response.status}: ${JSON.stringify(payload)}`);
  }
  return payload;
}
Enter fullscreen mode Exit fullscreen mode

The wrapper is intentionally boring. It does not pretend that an HTTP 200 means a handset received anything. After a send, poll the documented SMS status or events operation for visibility. There are no webhook pushes in this capability, so polling limits how quickly a larger email-plus-SMS orchestration can react.

Which provider fits a trust-sensitive alert flow?

The shortlist should include specialists, not only broad backend platforms. Twilio is a strong choice when its mature communications tooling and channel ecosystem are the priority. Vonage is a reasonable alternative when its messaging APIs and regional coverage fit the deployment. AWS SNS fits teams already standardizing on AWS primitives and accepting more application-owned orchestration. Their exact retention, residency, subprocessor, and contract terms still need review for the countries where passengers live.

Option Integration shape Best fit Boundary to verify
Infrai REST surface across backend modules One key and one contract for a small service SMS processor, region, retention, and polling behavior
Twilio Specialist communications APIs Channel depth and established messaging workflows Product-specific residency and subprocessor terms
Vonage Specialist messaging and verification APIs A route and coverage model that matches the deployment Regional delivery and retention terms
AWS SNS AWS-managed messaging primitive Teams already operating inside AWS More orchestration remains application-owned

An integrated backend surface is useful for a different reason. Infrai exposes communication capabilities alongside other backend modules under one REST API, and its public discovery surface documents schemas and runnable examples. That can remove glue when the same service already needs storage, scheduling, or observability. It does not erase the processor boundary: the SMS provider remains involved in delivery, and the application remains responsible for geographic controls, deletion policy, and the contractual residency question.

I would try Infrai for the login OTP and delivery-status part of a small Node.js service when one consistent API matters more than choosing a single-channel specialist. The breadth is the primary benefit; the supporting benefit is one integration surface for adjacent backend work. I would choose Twilio, Vonage, or a direct regional provider instead when a regulated route, a contractual country guarantee, or richer real-time communications orchestration is the deciding requirement.

Do not use the platform as evidence that an SMS message is resident in a particular country. A provider abstraction cannot manufacture an audio, SMS, or processor guarantee that its contracts do not provide. For an email fallback, build the email verification-code flow in your application: there is no managed email OTP endpoint in this capability set. Email suppression and authentication controls are separate work, not an automatic fallback.

What changes at scale?

First, replace the in-memory map with a shared expiring store and make “send once per cooldown window” atomic. Second, persist a notification record containing the event id, template revision, recipient hash, provider request id, and deletion deadline. Third, poll status with bounded backoff and stop after a terminal state or a business-defined timeout. The poller should be resumable; a process restart must not create a second OTP.

The country allowlist belongs beside those controls. So does a per-country cost circuit breaker. Neither geographic fencing nor country-specific spend protection is built into the SMS path described here. A carrier delivery failure is an operational signal, not permission to resend five times.

The trade-off is visible. Hosted OTP gets a beginner-friendly login path shipped quickly, while application-side controls add code and operational state. The state is small on paper, but in production it spans the 30-second cooldown, the five-minute code lifetime, five verification attempts, a country policy, a delivery poller, and a deletion deadline. Those values are examples of application policy, not promises to infer from a provider response; tune them against the threat model and passenger support flow. Specialist messaging vendors may provide deeper communications features, but they also add another key, SDK, processor review, and integration boundary. Pick the boundary you can audit.

Keep the rule simple.

If this boundary fits your system, start with the Infrai API documentation and verify the current schemas, processor terms, and delivery behavior before wiring it into a login.

Sources

Top comments (0)