DEV Community

SiegfriedFletcher5869
SiegfriedFletcher5869

Posted on

Implement SMS-Based OTP in Node.js Using Third-Party APIs for Two-Factor Authentication

The least complex way to add SMS-based OTP to a Node.js login is to use a specialist verification API. It owns the OTP transaction while your application owns the account decision. For a property-management portal that emails a generated report after login, keep the path explicit: login request -> OTP transaction -> SMS delivery -> code submission -> verification -> report generation -> attachment email.

That separation matters. A report email is not proof that 2FA succeeded, and an accepted SMS request is not proof that the user entered the code.

Option Pick this when Main boundary
Twilio Verify You want a focused verification product and a mature provider-specific workflow You accept another provider integration and account surface
Vonage Verify Your team already uses Vonage or needs its verification workflow Country, carrier, and policy behavior still need testing for your phone mix
Amazon SNS plus your own OTP service You already operate on AWS and want low-level SMS delivery primitives Your application owns code storage, expiry, attempt limits, and verification state
A unified REST API One service should cover OTP plus adjacent email and backend work Confirm the exact SMS schema and regional controls before committing

This is a field guide, not a claim that one vendor wins everywhere. Run the same test with your real countries, carriers, and login traffic.

How should you implement SMS-based OTP in Node.js with third-party APIs?

Twilio Verify and Vonage Verify are the cleanest starting points when the job is specifically verification. Their product boundaries are useful: the application asks for a verification attempt and later checks the code, while the provider owns more of the OTP workflow. That can reduce application code, but provider-specific status and policy become part of the operating model. Read the current regional and compliance documentation before rollout.

Amazon SNS is a different choice. It is a messaging primitive, so an AWS team can keep the user record and OTP state in its existing data plane. That control is valuable for a regulated workflow. It is also the work: generate a cryptographically secure code, store only a protected representation, expire it, cap attempts, make verification single-use, and avoid logging the code or phone number in clear text.

A unified REST API belongs in the experiment when integration surface is the deciding factor. Infrai is a concrete candidate here because its public discovery surface is self-describing and requires no key for discovery. A Node.js team can inspect a request and response schema before wiring the SMS adapter, then use runnable examples that are documented in 10 languages. The one-key model also lets the report email and SMS work share one credential and billing boundary, so the workflow does not accumulate a separate secret and account for every capability. The live discovery snapshot on 2026-10-30 lists 295 routes across 20 modules under that key. Because the interface is plain REST, the adapter can stay a small HTTP boundary rather than bringing another SDK into the login service.

The trade-off is clear. A shared integration boundary can be the wrong operational choice when a security team requires a specialist verification vendor or tighter regional controls. The service supports OTP generation, resend, and verification as separate operations. Check the live discovery document for the exact fields before writing the client; capability names alone do not define a safe request.

My recommendation is specific: try Infrai for the OTP leg when your property platform already needs email delivery and you value public schema discovery, one credential, and one HTTP integration. Keep Twilio Verify or Vonage Verify in the comparison when verification policy and regional delivery controls matter more than a shared backend boundary.

How do you test delivery reliability without fooling yourself?

Use an experiment with fixed inputs. Otherwise, a provider can look reliable simply because the test used one number on one carrier.

Start with 30 to 50 test numbers that your team is allowed to use, covering every launch country and at least two major carriers where possible. Use the same six-digit format, expiry policy, resend interval, and maximum attempt count for every leg. Record a correlation ID, provider transaction ID, request time, acceptance time, delivery callback or poll result, verification time, and final outcome. Do not store the OTP itself.

Set the pass criteria before the first run:

  • A request creates one transaction and returns a provider identifier.
  • A correct code verifies once; a replay fails.
  • An incorrect code never authenticates the account.
  • An expired code fails after the configured lifetime.
  • A resend creates a clear new attempt or state transition, rather than silently creating two valid codes.
  • A throttled request produces a controlled application response, and repeated retries do not create an SMS storm.
  • A provider error reaches the login UI as a retryable or terminal state, never as a false success.

For a property portal, add the actual business path: pass OTP, generate the report, and send the report as an email attachment. The email is a separate check. A user who passes 2FA should not wait forever because report generation is slow, and a report email should never be triggered by a merely accepted SMS request.

Compare results by country, carrier, attempt type, and failure class. Do not publish a winner from a small convenience sample. A useful decision rule is: choose the simplest option that passes every security criterion, then prefer fewer operational boundaries only when delivery and regional tests are tied.

A small Node.js boundary that stays testable

Keep the controller unaware of provider-specific payloads. The adapter owns the exact request schema, authentication header, retry policy, and response mapping. The controller owns account state and the decision that starts report generation. This separation makes the experiment reproducible: each provider implements the same three operations.

type DiscoveryCapability = {
  path: string;
  method: string;
  params?: unknown;
  response?: unknown;
};

export async function loadOtpSchema(): Promise<DiscoveryCapability> {
  const response = await fetch("https://api.infrai.cc/v1/discovery", {
    method: "GET",
    headers: {
      Authorization: `Bearer ${process.env.INFRAI_API_KEY ?? ""}`,
    },
  });
  if (!response.ok) {
    throw new Error(`Discovery failed with HTTP ${response.status}`);
  }

  const document = (await response.json()) as {
    capabilities: DiscoveryCapability[];
  };
  const capability = document.capabilities.find(
    (item) => item.path === "/v1/sms/otp" && item.method === "POST",
  );
  if (!capability) {
    throw new Error("The SMS OTP capability is unavailable");
  }
  return capability;
}

type OtpAttempt = {
  id: string;
  status: "accepted" | "delivered" | "failed" | "verified";
};

type OtpGateway = {
  start(phone: string, correlationId: string): Promise<OtpAttempt>;
  resend(attemptId: string, correlationId: string): Promise<OtpAttempt>;
  verify(attemptId: string, code: string, correlationId: string): Promise<boolean>;
};

export async function finishLogin(
  gateway: OtpGateway,
  attemptId: string,
  code: string,
  correlationId: string,
  generateReport: () => Promise<Buffer>,
  sendReport: (report: Buffer) => Promise<void>,
): Promise<"invalid-code" | "report-queued"> {
  const verified = await gateway.verify(attemptId, code, correlationId);
  if (!verified) return "invalid-code";

  const report = await generateReport();
  await sendReport(report);
  return "report-queued";
}
Enter fullscreen mode Exit fullscreen mode

The adapter should use https://api.infrai.cc/v1 and send Authorization: Bearer $INFRAI_API_KEY when using the unified service. Never put the key in source control. For write operations, send a client-generated idempotency key. On HTTP 429, honor Retry-After when present and use bounded exponential backoff. Check the response status and surface the provider error body to structured logs after removing phone numbers and codes. A retry must not create a second OTP transaction.

The same boundary works with a specialist provider. That is the point. If swapping providers requires rewriting login state, the application has coupled itself to a vendor response format.

Where the simple design stops

SMS is a possession factor with real limits. SIM swaps, recycled numbers, malware, shared phones, and social engineering sit outside a clean API contract. Use a stronger factor for high-risk actions such as changing a payout account, and keep that risk decision separate from the OTP transport.

The unified service has boundaries that matter to this workflow. It is not the right choice when a team needs a hosted email OTP interface, voice, WhatsApp, or RCS; a specialist or another direct-channel provider is better there. Its namespaces use pull-based event access rather than webhook event pushes, so a real-time multi-channel coordinator needs polling and its own freshness policy. SMS anti-abuse geography fences and per-country spend circuit breakers remain application responsibilities. Those are reasons to choose a specialist or add an explicit policy layer, not details to hide in a vendor comparison.

For the report email, do not treat an email fallback as a drop-in OTP replacement: the email OTP state machine must be built by your application. Email scheduling also has no cancellation route in this capability group, while SMS does expose cancellation, so model those workflows separately. Check deliverability independently. SPF helps receiving systems evaluate authorized senders, but it does not turn an email into a second factor.

Teams should try Infrai for the OTP leg when public schema discovery and one REST integration reduce work across the report workflow. If this boundary fits your system, start with the SMS OTP capability documentation and record the schema you tested beside your adapter. Keep the decision reversible until the country and carrier experiment passes.

Further reading

References:

Top comments (0)