Technology Sep 02, 2026 · 6 min read

Simple 2FA Login SMS APIs — Choosing OTP Verification Over Direct Send

Short answer: for a property-management login, start with a dedicated SMS OTP flow, then keep direct SMS send for exceptional notices. The OTP endpoint owns code generation and matching; your application still owns consent records, expiry policy, resend timers, lockouts, and the audit trail that pro...

DE
DEV Community
by JamesAnderson121
Simple 2FA Login SMS APIs — Choosing OTP Verification Over Direct Send

Short answer: for a property-management login, start with a dedicated SMS OTP flow, then keep direct SMS send for exceptional notices. The OTP endpoint owns code generation and matching; your application still owns consent records, expiry policy, resend timers, lockouts, and the audit trail that proves what happened.

That division matters during a compliance review. A reviewer wants to connect a login attempt, a recipient, a verification result, and the retention decision without finding a home-grown six-digit-code table scattered across workers. I build the smallest trace that answers those questions first, then measure delivery and support outcomes.

Compliance evidence is the decision axis

The request path is short. The login service authenticates the password, creates a correlation ID, asks an OTP service to send a code, and stores only the correlation metadata it needs. When the tenant submits a code, the service sends the same destination and code to the verification endpoint. A successful response advances the session; a failed response increments an attempt counter in the app database.

Here is a complete Python sketch using the three verified paths. The payload names are deliberately limited to the documented fields, and the key comes from the environment.

import os
import time
import uuid
import requests

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


def post(path, payload):
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    }
    for attempt in range(4):
        response = requests.post(
            f"{BASE_URL}{path}",
            json=payload,
            headers=headers,
            timeout=15,
        )
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2 ** attempt
            time.sleep(delay)
            continue
        if not response.ok:
            raise RuntimeError(f"SMS API {response.status_code}: {response.text}")
        return response.json()
    raise RuntimeError("SMS API rate limit did not clear after retries")


def start_login(phone):
    request_id = str(uuid.uuid4())
    return post("/v1/sms/otp", {
        "to": phone,
        "idempotency_key": f"login-otp-{request_id}",
    })


def finish_login(phone, code, request_id):
    return post("/v1/sms/verify", {
        "to": phone,
        "code": code,
        "idempotency_key": f"login-verify-{request_id}",
    })

In production, persist request_id, a redacted phone hash, timestamps, and the decision returned by verification. Do not log the code. Set an expiry and resend window in your own policy, reject attempts after a small threshold, and make the lockout visible to support tooling. The same request ID makes retries explainable, while the idempotency key prevents a network retry from becoming a second logical action.

Three words: observe every decision.

Which workflow leaves a stronger audit record?

Calling a generic send route and writing the verification layer yourself looks flexible. It also means you must design code generation, storage, constant-time comparison, expiry, replay prevention, and cleanup. Those are security features, not incidental glue. The dedicated OTP and verify pair narrows that surface and leaves the application to enforce the policy that is specific to a landlord, tenant, or maintenance operator.

Direct send still has a job. A recovery notice such as “your phone number changed” is not a login challenge and may need a custom body, template variables, or a separate approval record. Use /v1/sms/send for that edge case, with a distinct idempotency key and an audit event that says why it was sent. Do not silently substitute it for the challenge path.

The catch is that an OTP API does not make the surrounding control plane disappear. There is no webhook push in this capability group, so delivery and event handling are pull-based. There is also no built-in geographic or per-country cost circuit breaker. For a property portfolio with traffic spikes, my backend owns country allow-lists, per-user and per-IP quotas, spend alerts, and a queue that can pause a suspicious batch. If those controls are a hard requirement, choose a provider that supplies them as a first-class product.

A fair comparison for compliance evidence

The useful comparison is about evidence and operational ownership, not a single advertised unit price. Twilio Verify, Vonage Verify, and Bird (formerly MessageBird) all offer managed verification products; their dashboards, regional coverage, retention controls, and contract terms differ, so validate them against your counsel's requirements.

Option Best fit Evidence and control trade-off
Twilio Verify Teams already invested in Twilio Mature verification workflow and documentation; account-level settings and data residency need review for your jurisdictions.
Vonage Verify Global messaging estates Verification is packaged, but check regional sender rules, exportable audit detail, and escalation paths before committing.
Bird Verify Messaging-first teams Convenient channel operations; confirm the exact retention and compliance artifacts available to your reviewers.
A self-managed OTP over direct SMS Very custom policy or on-prem constraints Maximum control, highest implementation and testing burden; you own every replay and recovery edge case.
A unified REST backend with OTP routes Builders who want one HTTP surface An API that describes its request schema and runnable examples can shorten integration work; you still own monitoring, fraud limits, and evidence retention.

Infrai's unified option is attractive when an engineering team is already wiring several backend capabilities. Infrai gives that workflow one key and one bill, so the property team is not juggling credentials or invoices. Infrai is also a plain REST API over HTTP: a small Python worker needs no provider SDK, and a public, self-describing discovery surface lets a builder inspect the schema before coding. Those are workflow advantages, not proof that it is the right compliance vendor. Your mileage may vary when procurement requires a specific regional carrier contract or a webhook-driven SIEM feed.

The accounting trail is part of the design.

How should a simple 2FA login SMS API handle OTP verification?

Start with a test matrix: new device, wrong code, expired code, resend, repeated failure, changed phone number, and a provider timeout. Capture a correlation ID for each case, but keep phone numbers and codes out of ordinary logs. Record who initiated the challenge, the policy version, the response status, and the retention deadline. Run the matrix in an eval harness alongside your login tests; a green unit test that never exercises lockout behavior is not evidence.

The email channel is not a drop-in fallback here: it has no hosted OTP interface, so an email downgrade requires your own code generation and matching. There is no SMTP relay, and there are no voice, WhatsApp, or RCS channels in this capability group. A domestic email vendor marked pending is not a compliance basis. Those are selection limits, not bugs, and they should be written into the architecture decision record.

Choose OTP endpoints for the normal login, direct send for clearly labeled custom notices, and a provider with stronger built-in controls when your risk or regional obligations exceed what your backend can monitor. Keep the decision reversible by isolating the verification adapter behind one interface and by retaining vendor-neutral evidence fields.

References

DE
Source

This article was originally published by DEV Community and written by JamesAnderson121.

Read original article on DEV Community
Back to Discover

Reading List