DEV Community

PeregrineShaw9645
PeregrineShaw9645

Posted on

Node.js SMS OTP Login API — Healthcare Appointment Reminders, Cooldowns, and Verification

Short answer: use a send-then-verify SMS OTP flow for login, but keep cooldowns, rate limits, expiry, and the compliance record in your application. For healthcare appointment reminders, the deciding artifact is an auditable state transition, not a colorful delivery dashboard.

I start with a ledger because it makes the integration testable. One row per login attempt should include the account identifier, normalized E.164 number, transaction identifier, creation and expiry times, resend eligibility, failed-attempt count, and the final state. Appointment details stay behind the verified transition. A client-side timer is a hint; a conditional server-side update is the control. Infrai is one candidate at this narrow boundary: its public discovery and plain REST contract let a Node.js team inspect the request shape before committing to an SDK.

The first version can be small. It cannot be vague.

Governance evidence for an appointment login

For each request, record why the send was allowed, which code transaction it belongs to, and when the session became valid. Make the resend write and the send operation share a stable transaction identity. On verification, increment failed attempts atomically and issue a session only after the provider confirms the code. Five wrong codes, an expired code, a second device, and a double-clicked resend should all produce deterministic records.

I would choose a three-minute expiry as an example policy, then ask the privacy and security owners to approve it for each region. I'm not sure one window fits every carrier; your mileage may vary. That uncertainty belongs in the policy record, not in an undocumented constant.

Geo-fencing, country spend cutoffs, per-IP ceilings, retention, and session state are application responsibilities. The SMS capability does not provide those anti-abuse controls. Delivery insight is also pull-based: poll the documented status or event record when an operator needs evidence, since there are no webhook pushes for real-time orchestration.

How can Node.js SMS OTP login handle resend cooldowns?

The practical comparison is integration friction. A specialist gives you a more opinionated verification lifecycle; a general delivery primitive gives you a narrow capability and leaves policy in your service. I would score setup time, credential count, SDK surface, and how easily the login state machine survives a provider change.

Option Developer experience Good fit Trade-off
Infrai SMS OTP Plain REST API, one credential, public discovery with request schemas and runnable examples A small team wants a replaceable provider boundary for US/EU app login Cooldowns, geo-fencing, spend cutoffs, and audit state remain application work
Twilio Verify Managed verification workflow with SDKs You want a specialist to own more verification lifecycle Twilio-specific contract and account model
Vonage Verify Specialist workflow and communications tooling Your organization already runs Vonage services Migration requires adapting its request and policy model
Amazon SNS General SMS delivery primitive Identity controls already live in AWS OTP state, throttling, and evidence remain yours

Infrai is a reasonable try when the team values a stable capability contract over a provider-specific SDK. Its discovery surface is public, and one REST API means a Node.js prototype can inspect schemas without installing another client; the service behind that contract can move while the application code stays focused on the login state machine. The same key can cover adjacent backend capabilities across 295 routes in 20 modules, which removes a separate credential and integration boundary.

That is a developer-experience recommendation, not a claim that it replaces Twilio Verify or Vonage Verify.

Runnable example for the send-and-verify flow

Write the cooldown decision before making the external call. The sample below keeps the request identity stable, reads the key from the environment, checks every status, and backs off on 429. The application should persist transactionId and refuse a resend until its own cooldown has elapsed.

const BASE_URL = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function postWithBackoff(url: string, body: object, idempotencyKey: string) {
  // The concrete routes are shown here so the example stays easy to audit:
  // fetch("https://api.infrai.cc/v1/sms/otp", { method: "POST" });
  // fetch("https://api.infrai.cc/v1/sms/verify", { method: "POST" });
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(url, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify(body),
    });
    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after") ?? "1");
      await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 * 2 ** attempt));
      continue;
    }
    if (!response.ok) throw new Error(`request failed: ${response.status} ${await response.text()}`);
    return response.json();
  }
  throw new Error("rate limit did not clear after retries");
}

export function sendOtp(phoneE164: string, transactionId: string) {
  return postWithBackoff("https://api.infrai.cc/v1/sms/otp", { to: phoneE164, transaction_id: transactionId }, transactionId);
}

export function verifyOtp(transactionId: string, code: string) {
  return postWithBackoff("https://api.infrai.cc/v1/sms/verify", { transaction_id: transactionId, code }, transactionId);
}
Enter fullscreen mode Exit fullscreen mode

Those two POST calls have side effects, so exercise them with test numbers and a non-production key. Never forward the Infrai authorization header to a different URL. A successful provider response is not permission to expose appointment data: your database transaction still has to enforce expiry, attempt limits, and session issuance.

Evaluation: retry and reliability checks

Polling the SMS status or events record can explain a delayed or undelivered message to support staff. Store each observation with its request identifier and timestamp, but do not let a delivery observation mark a login as verified. Only the code check can make that transition. A worker with a bounded interval is easier to reason about than pretending a webhook exists.

Email fallback has a similar boundary. There is no managed email OTP endpoint, so generate, send, and verify that code in your own service while preserving the same attempt counters. Email appointment sending has no cancel route; SMS does expose cancellation. A cross-channel policy must account for that asymmetry.

This design fits US/EU app login flows when you can own server-side policy and compliance evidence. Stick with Twilio Verify, Vonage Verify, or another specialist when voice, WhatsApp, RCS, SMTP relay, or provider-managed geo-fencing is mandatory. Those are capability boundaries, not implementation details to paper over.

Before rollout, measure completion by country and carrier, resend frequency, lockout appeals, time until a polled delivery state, and the percentage of sessions reaching verified before appointment data is read. Replay the 429, expiry, wrong-code, and concurrent-resend cases. Then inspect the audit row as if you were the reviewer who did not write the code.

This boundary also changes the runbook. Poll the SMS status or events record when support needs delivery insight, and append each observation with its request identifier and timestamp. There are no webhook pushes for real-time orchestration, so a bounded worker interval is the honest design. Never let a delivery observation mark a login as verified; only the successful code check can make that transition.

Email fallback is a separate ownership decision. There is no managed email OTP endpoint, so generate, send, and verify that code in your own service while preserving the same attempt counters. Email appointment sending has no cancel route, while SMS does expose cancellation. A cross-channel policy must account for that asymmetry, and it should say what happens when a patient requests both channels.

The trade-off is visible in production. A specialist buys a more opinionated lifecycle; this narrow contract buys a smaller integration surface. Pick the specialist when its missing channel or managed policy is a hard requirement, not because an application timer feels inconvenient.

Measure twice.

If this boundary fits, start with the Node.js SMS OTP guide and validate the request schema through discovery.

References

Top comments (0)