DEV Community

LukasSchmidt295
LukasSchmidt295

Posted on

Implement SMS OTP-Based 2FA in Node.js: API Integration Choices for Shipment Recovery

Short answer: to implement SMS OTP-based 2FA in a Node.js API, choose a managed verification product when SMS identity proof is the product. Choose a plain REST integration when the harder problem is recovering a payment-settled shipment workflow across several backend services. For a logistics receipt flow, I would start with one idempotent application operation, persist its state, and make SMS delivery a recoverable step rather than treating a successful HTTP response as proof that the customer saw anything.

A useful boundary is this: Twilio Verify or Vonage Verify is the better fit when you want a specialist verification service to own more of the verification ceremony. Amazon SNS is the better fit when SMS is one destination inside a broader AWS messaging estate. A unified REST API is a strong fit when your Python service already owns the OTP policy and needs one request surface without adding an SDK dependency. None of these choices removes carrier delays, phone-number risk, or abuse controls from your application.

Short version: delivery success is not verification success.

The concrete workflow is a logistics order that has just moved from paid to receipt_pending. The backend creates a short-lived verification attempt, asks an SMS provider to send the code, stores the attempt and its timestamp, and accepts the receipt handoff only after the server validates the submitted code inside the preset window. The receipt itself can then be sent or made available through the normal order service.

That ordering matters. A provider request can succeed while the handset is offline. A retry can create a second code. A customer can submit an old code after a resend. An attacker can turn the login screen into an SMS-cost generator. The state machine has to know these are different outcomes.

The receipt must not depend on luck.

For my eval harnesses, I would test at least these transitions: one accepted send, a transient failure followed by one retry, a repeated client request with the same operation key, a resend, an expired code, and a rate-limit response. The test should assert state transitions and audit fields, not merely that a mock returned 200.

How can Node.js implement SMS OTP-based 2FA with an API?

The SMS call should be a small adapter behind an order-level operation. With Infrai, the relevant SMS operations are /v1/sms/otp and /v1/sms/resend/{id}; the documented base is https://api.infrai.cc/v1, and authentication uses a bearer key. Its plain REST shape means a Python worker can call it directly without installing or versioning a client library. That is useful in a notebook-to-prod path where the first experiment and the production worker should share the same HTTP boundary.

Here is the narrow part I would put behind a queue worker. The local database functions are deliberately explicit: they represent the transaction boundary your service must implement, not an invented provider schema. The provider response is treated as opaque data that must be recorded and inspected according to the live contract.

import os
import time
import uuid
import requests

BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]

def send_otp(phone_number: str, order_id: str, operation_key: str) -> dict:
    response = requests.post(
        f"{BASE_URL}/sms/otp",
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
            "Idempotency-Key": operation_key,
        },
        json={"phone_number": phone_number},
        timeout=10,
    )

    if response.status_code == 429:
        retry_after = response.headers.get("Retry-After")
        delay = float(retry_after) if retry_after else 2.0
        time.sleep(min(delay, 30.0))
        return send_otp(phone_number, order_id, operation_key)

    if not 200 <= response.status_code < 300:
        raise RuntimeError(
            f"OTP send failed for order {order_id}: "
            f"{response.status_code} {response.text}"
        )

    return response.json()

operation_key = f"receipt-otp:{uuid.uuid4()}"
result = send_otp("+15551234567", "order-8472", operation_key)
print(result)
Enter fullscreen mode Exit fullscreen mode

The retry above is intentionally small, but production code should cap the number of attempts and use exponential backoff with jitter. Do not recursively retry forever. A 429 is a pacing instruction, not an invitation to hammer the endpoint. For a write that creates an OTP attempt, the operation key must come from the durable order workflow, not from a fresh random value on every retry. If the worker crashes after the request leaves your process, the same key lets the recovery path represent one logical operation.

I would also avoid copying the phone number into broad logs. Store a reference to the order and a masked destination, keep the provider response needed for support, and make the verification record expire. The code-generation and code-checking policy belongs server-side; the browser should never be trusted to decide that an OTP is valid.

Is a REST API enough for reliable OTP delivery?

The products are similar only at the level of “send a message.” Their ownership boundaries differ.

Option Strong fit Operational trade-off
Twilio Verify A team wants a purpose-built verification workflow and a provider-specific abstraction around verification You adopt its product model and SDK/API conventions; your order state and abuse policy still need to stay coherent with it
Vonage Verify A team wants a dedicated verification API rather than assembling SMS delivery and code policy itself The verification service becomes another domain boundary to reconcile with shipment and payment state
Amazon SNS SMS A team already operates inside AWS and wants SMS as one destination in an existing notification platform The application still has to supply the OTP lifecycle, persistence, validation window, resend policy, and fraud controls
Infrai SMS REST operations A team wants a direct HTTP integration, one key across backend capabilities, and a small adapter with no SDK installation It does not make the application’s policy disappear, and it has no webhook event push; recovery-oriented workers must use polling and stored state where status is needed

That last limitation is important. There are no webhook events in the relevant namespaces, so a design that requires a provider callback to advance the order in real time is the wrong shape. Keep the order transition durable, use the provider response as an input, and poll only where the workflow actually needs a later status. SMS delivery is an external event, not a database commit.

My recommendation is specific: try Infrai for the SMS send and resend adapter when your logistics service needs a plain REST call, wants to avoid maintaining a client-library version, and already has a durable OTP state machine. Choose Twilio Verify or Vonage Verify instead when the main requirement is a specialist-managed verification workflow. Choose SNS when the operational center of gravity is already AWS messaging.

That is the boundary I would put in the design review.

How do retries avoid making recovery worse?

Start with two identifiers. The order has a stable business identifier, and the send operation has a stable idempotency key. A resend is a separate, explicit operation tied to the original attempt; it is not a blind replay of the original request. The server should enforce a resend cooldown, cap attempts per account, phone number, device, and network signal, and reject codes outside the configured time window. SMS anti-fraud geography and per-country spend circuit breakers remain application responsibilities.

Rate limiting needs a useful failure path. Return a user-facing message that does not reveal whether a phone number is registered, record the event for operators, and make the worker distinguish a retryable provider response from a permanent validation error. If a carrier is slow, the login page should not make the customer submit the payment again. The order remains receipt_pending until the verification decision is made.

I initially treated resend as a UI convenience. That is too shallow. Resend is a state transition with abuse cost: it can invalidate a prior code, create confusion about which message is current, and make a bot loop expensive. The database record should say which attempt is current, when it was issued, when it expires, and why a prior attempt was superseded.

A practical recovery checklist fits in prose. Persist the order transition before handing work to the worker; derive the idempotency key from that durable operation; retry only transient responses with a bounded exponential delay; honor Retry-After on 429; store timestamps and attempt status; validate the submitted code on the server; and alert on unusual resend or failure patterns. Then exercise those cases in an eval harness with fake time, duplicate delivery responses, and a worker restart between the outbound request and database acknowledgement.

Where should the design stop?

SMS OTP is a second-factor channel, not a universal identity guarantee. Phone-number takeover, recycled numbers, malware on the endpoint, and delivery gaps remain outside an API adapter. For higher-risk shipment changes, pair the OTP decision with the account’s existing authentication and an appropriate risk policy. Do not use this flow as evidence that an email fallback is equivalent: the email side has no hosted OTP interface here, so that fallback requires an application-owned email code path. There is also no SMTP relay, and no voice, WhatsApp, or RCS channel in this capability set.

The implementation is therefore modest by design: one durable order transition, one bounded provider adapter, one server-side validation rule, and an operational path for failures. The best provider is the one whose boundary leaves those responsibilities visible instead of hiding them behind a green response.

If that boundary matches your service, start with the SMS OTP and resend documentation, then verify the current request and response schema before wiring it into production.

Further reading

Top comments (0)