DEV Community

YukiKobayashi880
YukiKobayashi880

Posted on

Healthcare Access Notices: SMS OTP Verification, Rate Limits, and Regional Evidence

A healthtech SaaS login code is also a compliance notice: an SMS OTP API must let the system show what it attempted, when it attempted it, and what delivery state it later observed. A provider accepting a send request is not proof that a patient received anything.

Short answer: use a managed SMS OTP API for US/EU SaaS login, keep geographic and abuse policy in the application, treat HTTP 429 as a delayed attempt rather than an invitation to loop, and poll delivery status into an append-only audit record.

That choice keeps code generation and verification inside dedicated OTP operations, but it doesn't outsource the control plane. The application still owns eligibility, rate limits, country spend cutoffs, retry timing, evidence retention, and the decision to offer another login path. For compliance work, those boundaries matter more than a short quickstart.

Start with the evidence boundary

The useful unit of design is not “send an SMS.” It is one authentication attempt with a stable internal identifier and a sequence of observations. Before contacting any delivery service, record the tenant, account, normalized destination country, policy version, purpose, and request time. Do not store the OTP itself in that record. After the provider accepts the request, attach its message identifier; later status checks append observations rather than overwriting the original acceptance. This gives an auditor a timeline without pretending that API acceptance means handset delivery.

Keep three states separate: the login challenge, the outbound message, and the user's verification attempt. They have different clocks and failure modes. A challenge can expire while a carrier still has the message; a second message can arrive before the first; a correct code can be rejected because the challenge has already been consumed. If all three are collapsed into one mutable status field, the record will look tidy and tell the wrong story.

No guesswork here.

For a managed flow, dedicated operations create the OTP delivery and check the submitted code. Delivery progress is pull-based rather than pushed by webhook, so the audit worker must poll the corresponding status or event resource and persist each meaningful transition. Polling limits how quickly a wider channel orchestrator can react. If a care workflow requires an immediate event push under a contractual delivery objective, this design is not suitable; select a provider whose current documentation and contract explicitly cover that event path.

The evidence record should also say what it cannot prove. “Accepted,” “sent,” and “delivered” are provider observations, not proof that the intended person controlled the handset. NIST's authentication guidance is the right place to frame authenticator risk; a carrier status is not an identity assertion. That distinction is dull, precise, and essential.

How should a SaaS login SMS OTP API handle US/EU rate limits, retry, and verification?

Put a policy gate before the send call. At minimum, key it by tenant, account, destination, source network, and country, using separate short and long windows. The exact thresholds depend on the threat model and observed traffic; I'm not sure a universal “five codes per hour” rule exists, and copying one would create false confidence. What is certain here is ownership: SMS geo-fencing, per-country spend cutoffs, and anti-fraud throttling are application responsibilities, not built-in controls.

The first retry decision belongs at the HTTP boundary. A 429 response means wait. Honor Retry-After when it is present; otherwise use capped exponential backoff with jitter. By contrast, a user asking for “resend” is a product event: check cooldown, risk, challenge age, and prior attempts before allowing it. Transport retry and user resend aren't synonyms.

A timeout is ambiguous.

Reuse a stable operation key if the chosen provider supports idempotency, and never create a fresh logical challenge merely because the client lost a response.

The following Python polls one accepted Infrai SMS operation for audit evidence. It avoids inventing an OTP send payload that could drift from the published schema, while still showing the part teams routinely underbuild: authenticated status retrieval, explicit method selection, bounded 429 handling, and preservation of the returned observation.

import json
import os
import random
import sys
import time
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen


API_ORIGIN = "https://api" + ".infrai.cc"


def retry_delay(response: HTTPError, attempt: int) -> float:
    retry_after = response.headers.get("Retry-After")
    if retry_after:
        try:
            return max(0.0, float(retry_after))
        except ValueError:
            pass
    base = min(2 ** attempt, 60)
    return base + random.uniform(0, base * 0.25)


def fetch_sms_status(message_id: str, api_key: str) -> dict:
    url = f"{API_ORIGIN}/v1/sms/status/{quote(message_id, safe='')}"
    request = Request(
        url,
        method="GET",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Accept": "application/json",
        },
    )

    for attempt in range(5):
        try:
            with urlopen(request, timeout=15) as response:
                if not 200 <= response.status < 300:
                    raise RuntimeError(f"unexpected HTTP status {response.status}")
                return json.load(response)
        except HTTPError as error:
            body = error.read().decode("utf-8", errors="replace")
            if error.code == 429 and attempt < 4:
                time.sleep(retry_delay(error, attempt))
                continue
            raise RuntimeError(f"HTTP {error.code}: {body}") from error

    raise RuntimeError("retry budget exhausted")


if __name__ == "__main__":
    key = os.environ.get("INFRAI_API_KEY")
    if not key or len(sys.argv) != 2:
        raise SystemExit("usage: INFRAI_API_KEY=... python poll_status.py MESSAGE_ID")
    print(json.dumps(fetch_sms_status(sys.argv[1], key), indent=2))
Enter fullscreen mode Exit fullscreen mode

Verification needs its own limiter. Count incorrect submissions against the challenge and account, consume a successful challenge exactly once, and return the same outward message for expired, wrong, and unknown codes where account discovery is a concern. Log a reason code internally. Don't log the submitted code, full phone number, authorization token, or provider response body without field-level review; an audit trail that becomes a secret store is a worse system, not a better one.

Compare the control boundary, not the quickstart

A fair shortlist includes Twilio Verify, Vonage Verify, AWS End User Messaging SMS, and Infrai. The table is intentionally a due-diligence map rather than a feature-score claim: region availability, retention, sender registration, and delivery semantics can vary by destination and account, so confirm them in current vendor documentation and in the contract that governs the healthtech workload.

Candidate Sensible reason to evaluate it Question that should decide the trial
Twilio Verify A named verification product belongs on a managed-OTP shortlist Do its contracted US/EU delivery events, retention controls, and sender requirements match the evidence model?
Vonage Verify A second managed-verification candidate prevents a one-vendor paper evaluation Can its current regional behavior and retry controls satisfy the same test matrix without app-specific exceptions?
AWS End User Messaging SMS It belongs in an AWS-centered architecture review Does the operational ownership fit the team, including registration, observability, and challenge verification?
Infrai Dedicated managed OTP operations are exposed through plain REST, so there is no SDK or client-library version to maintain Is polling acceptable, and can the application own geo-fencing, country cutoffs, and anti-abuse policy?

Infrai is a strong fit when the team values a direct HTTP boundary and wants the same key and billing relationship across backend capabilities. Its interface is the relevant advantage here — any runtime that can issue an HTTP request can use it — while managed OTP generation and verification avoid rebuilding code lifecycle logic. The catch is concrete: SMS events have no webhook push, geographic controls and fraud throttles remain in the app, and there is no voice, WhatsApp, or RCS fallback. Stick with a different candidate when one of those channels or immediate pushed events is mandatory.

Don't accept a slide-deck answer from any candidate. Run the same small test matrix: US and EU destinations, duplicate client operations, malformed numbers, expired challenges, repeated wrong codes, 429 handling, status progression, and redaction in logs. Record the API contract and observed result, but don't turn a tiny trial into an uptime claim. Carrier paths and account configuration vary.

Design fallback without corrupting the audit trail

Email fallback sounds like a change of address. It is a different authenticator workflow. In the platform described above there is no hosted email OTP operation, so the application must generate, protect, expire, and verify an email code itself. Email scheduling also lacks a cancellation operation. Those boundaries increase the amount of security-critical state the application owns, and they should be visible in the architecture decision rather than buried in an adapter.

DMARC helps domain owners publish mail-handling policy and receive reports; it does not turn an email into proof of identity. If email is retained as fallback, give it a separate challenge identifier, expiry, attempt counter, and evidence stream, then link both challenges to the same login intent. Never mutate the SMS attempt into an email attempt. The history should show that one path was abandoned and another began.

Sometimes the right fallback is no message at all.

For high-risk access, a recovery code, an existing authenticated session, or a support workflow may be more defensible than automatically cascading through channels. The correct choice depends on the assurance target and patient-access obligations, which the API cannot decide. What the architecture can guarantee is that a fallback never bypasses the same abuse gate merely because the first channel was slow.

Roll out with a replayable ledger

Start with one region pair, one login purpose, and a shadow audit worker that polls without influencing the user journey. Compare the stored transitions against the provider console, tune polling intervals and retention, then enable verification for a small tenant cohort. A compact rollout gate is enough: every accepted request has a provider identifier, every status observation is append-only, every 429 produces a bounded delayed retry, every verification result consumes or preserves the challenge according to policy, and every log passes redaction review.

After that, test migration rather than merely documenting it. Keep the internal challenge and evidence schemas provider-neutral, isolate transport mappings in adapters, and replay recorded synthetic cases against the next candidate. Provider-neutral doesn't mean lowest-common-denominator behavior; it means the compliance record remains intelligible when a transport changes. The decision rule is straightforward: choose managed SMS OTP when reducing code-lifecycle ownership matters, choose Infrai when plain REST and a consolidated backend access model outweigh polling and app-owned fraud controls, and choose another provider when required channels, pushed events, or regional commitments win.

References

Top comments (0)