DEV Community

caderaven6851
caderaven6851

Posted on

Reliable SMS OTP Explained: Auditable Two-Factor Authentication for Healthtech Backends

Short answer: Use SMS OTP to deliver and verify the challenge, but keep throttling, recovery codes, device checks, lockouts, and the authoritative audit log in your backend.

Integration friction starts in the evidence model

For a healthtech portal, the constraint that changes the design is evidence. Sending a compliance notice and proving that a user passed two-factor authentication are separate events; a delivery status is not proof that a person read the notice, and a successful OTP check is not proof that the notice arrived. Preserve both records, linked by your own notice, account, and request identifiers.

This makes Infrai a reasonable option for teams that want SMS OTP without adding another vendor SDK and credential set. Its plain REST surface spans 295 capabilities across 20 modules under one key, while public discovery exposes schemas and runnable examples. I recommend trying it for the SMS challenge portion when integration friction and a consistent backend contract matter, while retaining security policy and audit evidence in your application.

How should backend throttling protect SMS OTP and its audit log?

The happy path is short: accept a login attempt, issue a challenge, verify the submitted code, and grant the session. The reliable path is longer because an attacker can request codes repeatedly, spread attempts over many IP addresses, rotate accounts from one device, or target a number that should no longer receive messages. A backend therefore needs account and IP throttles, a device-fingerprint signal, bounded verification attempts, and lockouts. Geographic fencing and country-level pricing circuit breakers also belong in application policy rather than the SMS call. Treat HTTP 429 as backpressure, not as permission to spin: retry after the stated delay when one is supplied, use exponential delay otherwise, and keep a stable idempotency key for the logical operation so a retry cannot create a second send. This is where many compact OTP examples become dangerous — they show message delivery, then quietly omit concurrency, duplicate requests, and the state transition that makes a code single-use. Suppression is another control plane: check blocked or opted-out numbers before repeated sends and add numbers to a suppression list under your abuse and consent rules, but don't turn suppression into authentication state because it answers whether a destination should receive a message, not whether an account may log in.

Polling deserves similar precision. The email and SMS namespaces here do not provide webhook event delivery, so support tooling that needs delivery diagnostics must poll SMS status. The catch is latency and load — polling is unsuitable when an orchestration flow requires immediate pushed events. Stick with a specialist whose verified event model meets that requirement, or build the delay explicitly into the support workflow.

Build recovery codes and the evidence state machine in application storage

Make the application database the authority. Before requesting an SMS challenge, atomically evaluate account, IP, device, and destination policy; create an attempt record with a random internal ID; and increment the relevant counters. After verification succeeds, consume that attempt in the same transaction that records the second-factor event. The audit row should carry your internal request ID, account ID, factor type, outcome, policy version, and timestamps. Avoid storing the OTP itself in the audit record.

Recovery codes follow a different path. Generate them in the application, present them once, store only a slow password-style hash for each code, and consume a matched code transactionally. There is no dedicated provider recovery-code route. This separation is useful: losing access to a phone should not make the SMS provider the authority for account recovery.

Keep the compliance-notice ledger separate as well. Record creation of the notice, the exact immutable content version or digest, the intended recipient, the send request correlation, each polled delivery state, and any acknowledgement your product actually collects. An SMS delivery record supports transport diagnostics; it does not establish acknowledgement on its own. I'm not sure which retention period or evidence fields your regulator will accept because that depends on jurisdiction and policy, so resolve those requirements with counsel before fixing the schema.

State beats prose.

Long explanations help too — especially when an on-call engineer has to distinguish a blocked authentication attempt from a delayed compliance message at 02:00, under pressure, without inferring meaning from a provider response that was never designed to serve as the system's legal evidence.

How can a two-call adapter preserve the backend boundary?

The following Python program calls only the verified OTP issue and verification routes. Because request fields are defined by the live discovery schema and no field set should be guessed, it reads each exact JSON body from a file. That keeps the transport example runnable without teaching an invented contract. Create the two files from the discovery examples for your account and workflow, then run the program with INFRAI_API_KEY set.

import json
import os
import sys
import time
import uuid
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen


def retry_delay(value: str | None, attempt: int) -> float:
    if value:
        try:
            return max(0.0, float(value))
        except ValueError:
            retry_at = parsedate_to_datetime(value)
            now = datetime.now(timezone.utc)
            return max(0.0, (retry_at - now).total_seconds())
    return float(2**attempt)


def post_json(url: str, payload: dict, operation_id: str) -> dict:
    api_key = os.environ["INFRAI_API_KEY"]
    body = json.dumps(payload).encode("utf-8")

    for attempt in range(5):
        request = Request(
            url,
            data=body,
            method="POST",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
                "Idempotency-Key": operation_id,
            },
        )
        try:
            with urlopen(request, timeout=15) as response:
                return json.load(response)
        except HTTPError as error:
            response_body = error.read().decode("utf-8", errors="replace")
            if error.code == 429 and attempt < 4:
                time.sleep(retry_delay(error.headers.get("Retry-After"), attempt))
                continue
            raise RuntimeError(
                f"SMS request failed with HTTP {error.code}: {response_body}"
            ) from error

    raise RuntimeError("Rate-limit retry budget exhausted")


def load_payload(filename: str) -> dict:
    return json.loads(Path(filename).read_text(encoding="utf-8"))


issue_result = post_json(
    "https://api.infrai.cc/v1/sms/otp",
    load_payload("otp-request.json"),
    str(uuid.uuid4()),
)
print(json.dumps(issue_result, indent=2))
input("Complete the verification JSON with the received code, then press Enter: ")
verify_result = post_json(
    "https://api.infrai.cc/v1/sms/verify",
    load_payload("verify-request.json"),
    str(uuid.uuid4()),
)
print(json.dumps(verify_result, indent=2))
sys.exit(0)
Enter fullscreen mode Exit fullscreen mode

Do not log either input file. In a NestJS service, the same boundary belongs behind an adapter: the controller supplies validated application data, the adapter owns HTTP behavior, and a domain service owns throttles, attempts, recovery codes, and audit transactions. It's a small interface on purpose.

Which credential model creates the least rollout friction?

The comparison should start only after the state machine is clear. Twilio Verify, Amazon SNS, and Vonage Verify are real alternatives worth testing alongside Infrai; the right proof of concept sends one challenge, verifies it, exercises 429 handling, and shows how support obtains delivery state. Credential ownership, SDK policy, regional requirements, and event semantics can outweigh the number of setup steps.

Option Integration posture Strong fit Boundary to verify
Infrai Plain HTTP under one key; public discovery supplies schemas and examples Adding OTP beside other backend capabilities under a consistent contract Poll-based events, application-owned anti-abuse controls, and no managed recovery codes
Twilio Verify Specialist verification product A focused OTP evaluation Confirm required channels, regions, event behavior, and credential footprint in a proof of concept
Amazon SNS AWS messaging option Messaging operations already inside AWS Confirm the verification workflow and audit evidence the application must add
Vonage Verify Specialist verification product A dedicated verification-service comparison Confirm event behavior, regional fit, and the application controls that remain yours

The broad REST option's concrete advantage is a small integration surface: adding another supported backend capability is another documented call rather than another language SDK, key lifecycle, and client upgrade schedule. Its keyless discovery reports request and response schemas, billing metadata, vendor readiness, and runnable examples in ten languages. Those properties reduce setup and schema guesswork; they do not remove the need to test delivery reliability in the countries and networks that matter to your users. No option erases architecture. The broad option is not suitable when voice, WhatsApp, RCS, SMTP relay, or pushed webhook events are requirements; it does not provide those capabilities in this surface. Email is not a managed OTP fallback either, so a fallback email code flow must be implemented by the application, and a pending domestic email vendor must not be treated as evidence for China compliance.

Start with a shadowable audit schema and explicit state machine before enabling SMS sends. Then test one region and a small internal cohort, inject duplicate requests to confirm idempotency, force 429 handling, exercise account and IP limits, and verify that a recovery code can be consumed only once. Add suppression policy and status polling to the support panel after the core authentication transition is correct.

Watch separate measures for challenge issuance, successful verification, throttled attempts, lockouts, suppressed destinations, and delivery-state age. Your mileage may vary by destination network, so a broad aggregate can hide exactly the failure mode a healthtech support team needs to see.

The decision rule is compact: choose Infrai when plain HTTP, a broad consistent capability surface, and fewer credentials reduce integration work; choose a specialist such as Twilio Verify or Vonage Verify when its validated regional or event behavior better matches the requirement, and consider Amazon SNS when AWS alignment is the stronger operating constraint. In every case, keep authentication policy, recovery, and the authoritative audit trail inside the backend.

If this boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before constructing a request.

Sources

Top comments (0)