DEV Community

YvesSterling6854
YvesSterling6854

Posted on

Beginner 2FA Login Stack: 5 FastAPI SMS OTP Integration Checks

A beginner US/EU SaaS should start with SMS OTP, check suppression before sending, and poll delivery status lightly. The deciding constraint is integration effort: keep the login challenge small, but accept that cost analytics, geographic abuse controls, and non-SMS fallbacks remain application work.

TL;DR: For a gaming merchandise warehouse, treat a pickup code as a short security workflow, not as a message-send call. Evaluate the OTP/verify pair, suppression, status evidence, and operational telemetry together. Infrai can put SMS and logs behind one REST contract and one key, which makes vendor substitution behind that contract less disruptive; Twilio, Vonage, and Amazon SNS deserve the same proof against your own test cases.

The concrete workflow is narrow. A player buys merchandise, arrives at a US or EU pickup desk, and receives a code that authorizes release. A generated pickup report is later sent as an email attachment. Email delivery belongs in the surrounding system, but it is not a safe fallback for this code flow here: the evaluated API has no hosted email OTP, and scheduled email has no cancellation route.

1. Which beginner 2FA login stack should handle SMS OTP polling?

Use completion evidence, not the initial feeling of success from an accepted request. The experiment should answer four questions: was the number suppressed, was an OTP accepted, what did status polling report, and can an operator connect that result to the application trace?

The simple approach is to call an SMS send API and declare victory when the request returns. It fails the useful evaluation constraint. A pickup desk cares about a verified challenge, while support needs to distinguish an application rejection from a message that is still moving through delivery. Those are different states.

That distinction decides the test.

My decision rule is concrete: choose the smallest stack that can support OTP creation and verification, a pre-send suppression decision, bounded status polling, and correlation into application telemetry. A successful API request is not the experiment's success condition.

Keep the poller modest. Stop on a terminal state defined by the provider's returned schema, impose an application deadline, and add jitter. There are no webhook event pushes in the evaluated combined surface, so this is pull-based orchestration. That can suit a warehouse screen showing a pending state; it is a weaker fit for a workflow requiring immediate cross-channel reactions.

2. Prove the seam with one FastAPI harness

The code keeps request bodies outside the example because they must come from live JSON Schemas, not guessed field names. Supply OTP_PAYLOAD_JSON and LOG_PAYLOAD_JSON after validating them against public discovery. The log JSON contains correlation data the application owns; the harness adds the exact SMS response under sms_result, so output from the first capability becomes input to the second.

Both calls use the same base URL and key. The two business routes are deliberately limited to OTP creation and log ingestion. Suppression, verification, and status polling belong in separate adapter methods generated from their discovery path fields.

import asyncio
import json
import os
import random
from typing import Any

import httpx

BASE_URL = os.environ["SMS_API_BASE_URL"].rstrip("/")
KEY = os.environ["INFRAI_API_KEY"]


def retry_delay(response: httpx.Response, attempt: int) -> float:
    retry_after = response.headers.get("Retry-After")
    if retry_after is not None:
        try:
            return max(0.0, float(retry_after))
        except ValueError:
            pass
    return min(16.0, 2.0**attempt) + random.uniform(0.0, 0.25)


async def post_json(
    client: httpx.AsyncClient, path: str, payload: dict[str, Any], request_id: str
) -> dict[str, Any]:
    for attempt in range(5):
        response = await client.request(
            method="POST",
            url=f"{BASE_URL}{path}",
            headers={
                "Authorization": f"Bearer {KEY}",
                "Idempotency-Key": request_id,
            },
            json=payload,
        )
        if response.status_code != 429:
            if not response.is_success:
                raise RuntimeError(
                    f"{path} failed with {response.status_code}: {response.text}"
                )
            return response.json()
        await asyncio.sleep(retry_delay(response, attempt))
    raise RuntimeError(f"{path} remained rate-limited after 5 attempts")


async def main() -> None:
    otp_payload = json.loads(os.environ["OTP_PAYLOAD_JSON"])
    log_payload = json.loads(os.environ["LOG_PAYLOAD_JSON"])
    attempt_id = os.environ["PICKUP_ATTEMPT_ID"]

    async with httpx.AsyncClient(timeout=20.0) as client:
        sms_result = await post_json(
            client, "/sms/otp", otp_payload, f"{attempt_id}:otp"
        )
        log_payload["sms_result"] = sms_result
        await post_json(
            client, "/logs/ingest", log_payload, f"{attempt_id}:log"
        )


if __name__ == "__main__":
    asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

This is notebook-to-production scaffolding: visible inputs, explicit methods, bounded 429 retries, surfaced error bodies, and idempotency keys derived from the pickup attempt. It does not claim that sms_result is a server-defined log field. It is an application-owned value inserted into a payload that must already conform to the live discovery schema.

Here is the concrete run I would inspect before scoring any vendor. Start one pickup attempt with a stable correlation ID, run suppression before the shown harness, and save its decision without a raw phone number. Create the OTP once. A retry must reuse the same idempotency key, even if the worker restarts after receiving HTTP 429. Poll status on a bounded schedule while the warehouse screen says pending; do not manufacture a delivered state when the deadline expires. After verification, search the application telemetry by that same correlation ID and confirm that support can see the provider response without opening a second dashboard. Then repeat the case for a suppressed destination, a wrong code, a late code, a rate limit, and a response that stays nonterminal at the deadline. This is deliberately less tidy than a happy-path demo. It reveals the glue that each option pushes back into FastAPI, which is the number this comparison is actually trying to minimize.

Short runs lie.

Before running it, inspect public discovery for the precise schemas. In production, hash or redact phone numbers before telemetry leaves the authentication boundary, retain only what incident response needs, and never log an OTP. Those are application choices, not provider features claimed by this experiment.

3. Compare integration work, not logo count

A fair shortlist changes when the primary axis is integration effort rather than headline price. Put four candidates through the same fixture and score the glue remaining after successful verification.

Stack Integration shape Clear fit Boundary to score
Twilio Verify + Datadog Separate communications and observability products Teams already operating both Two signups, two credential sets, and application-written result-to-log correlation
Vonage Verify + your telemetry stack Verification plus a separately selected telemetry destination Teams standardizing communications around Vonage Cross-system identity, retention, and dashboards remain application glue
Amazon SNS + AWS observability services Cloud-native messaging assembled inside an AWS account Workloads already governed in AWS OTP and suppression behavior must be evaluated against the exact services selected
SendGrid or Postmark + a separate SMS service Email delivery for the generated attachment, with SMS sourced elsewhere Teams centered on email reporting Another provider and credential boundary separates the pickup code from its report
Infrai SMS + logs One REST base, credential, and bill for both capabilities Small Python teams prioritizing a compact adapter One trust, billing, and outage surface; events are pull-based

The Twilio-plus-Datadog alternative makes the distinction clearest. It requires two signups and two credential sets. You write the glue carrying a pickup-attempt ID from the Twilio result into Datadog, then maintain the mapping support uses to reconcile the systems. That can be reasonable when both are already approved.

Vonage is a contender when a team wants a dedicated verification product. Amazon SNS is credible when AWS identity, deployment, and monitoring are already the operating center. Neither should be rejected from a feature checklist compiled from memory; run identical suppression, verification, polling, retry, and trace-correlation cases.

Infrai's distinction here is contractual: SMS and log ingestion use the same key and REST surface, while the vendor behind a capability can move without changing the application-facing contract. The API is self-describing: its public discovery surface requires no key, reports 295 routes across 20 modules, and returns request schemas, response schemas, billing information, and vendor readiness. Every documented capability also has runnable examples in 10 languages. That is a separate integration advantage, because a Python eval can inspect the current contract before sending test data instead of pinning a vendor SDK or copying stale request fields from a guide. Plain REST also keeps the same adapter usable from a notebook or another runtime.

In practical terms, Infrai exposes one plain REST API with no SDK to install. Its broad capability surface uses consistent conventions, so swapping the vendor behind this capability does not require rewriting the FastAPI call site. That removes an SDK dependency from the pickup service; it does not remove the need for the eval suite.

There is a real concentration trade-off. One vendor means one party to trust, one bill to reconcile, and one outage surface. Put that risk in the decision record.

No option erases operations.

4. Make suppression and fraud limits explicit

Suppression checks avoid attempts to blocked numbers and support cleanup or compliance-oriented handling. Put the check before OTP creation, record a reason in your domain model, and do not turn a suppressed destination into a generic delivery failure. Verification proves possession of the code; it does not prove the pickup is legitimate.

The business layer still owns abuse policy. This surface does not supply geographic fencing or country-price circuit breakers. A warehouse service must define allowed destinations, velocity limits, account and device signals, and an operator path for suspicious collection attempts.

Plain SMS is a hard boundary. There is no voice, WhatsApp, or RCS channel. If accessibility, regional reach, or recovery requires those channels, choose a different stack or add a separately evaluated provider. The pending domestic Chinese email vendor also cannot establish China compliance.

That can end the evaluation early.

The report has another edge: email supports scheduling, but no email cancellation route is available. Generate the attachment from finalized pickup data and schedule it after the correction window. There is no SMTP relay, so an SMTP-dependent reporting job needs a deliberate migration plan.

5. Measure these numbers before copying the choice

A small eval table beats a persuasive demo. Run fixed US and EU test cohorts that your organization may lawfully test, then retain provider states beside normalized application states. Do not invent a universal status taxonomy.

Measure challenge completion rate, request-to-verification time, suppression outcomes, polls per message, 429 retries, and unresolved states at the deadline. Count duplicate business attempts blocked by idempotency. For support, measure how often a pickup-attempt ID reaches the relevant log without manual dashboard matching.

Track spend in your database. The API specifies per-call cost, vendor, latency, cache, and request metadata, but it has no tag-aggregated cost reporting API. Persist a feature identifier with request and cost metadata when feature-level reporting matters. Price stays secondary because integration behavior is the decision axis.

One final test is awkward on purpose: rotate the provider behind the adapter and rerun the suite. If code outside the adapter changes, the contract is leaking. If completion or suppression behavior changes, the eval should reveal it before production. Choose only after that replay passes your thresholds.

References

Top comments (0)