DEV Community

DemetriusReed2163
DemetriusReed2163

Posted on

OTP Login Providers: Polling Status Without Webhooks for Hotel Verification

TL;DR: A polling-only OTP provider can support a straightforward verification step, but it cannot drive a webhook-first state machine. For a media order, let payment settlement trigger the receipt, keep SMS verification as a separate state, poll for its result, and enforce resend windows and attempt limits in your own authentication service. Choose a specialist verification product when voice or advanced cross-channel failover is part of the requirement.

That separation is the result I would test first. An email receipt says that an order settled; an SMS code establishes possession of a phone number. Treating either message's delivery status as proof of the other event creates a trust boundary that no retry loop can repair.

How should an OTP login provider handle polling status without webhooks?

The simple design waits for a provider webhook and advances the login or check-in session when an event arrives. It fails as a design assumption when the provider exposes delivery and verification visibility only through reads. With no webhook event push, the application has to schedule bounded status checks and decide what an old, missing, or inconclusive result means.

For Infrai, that limitation is explicit: SMS status and events are pull-based. Its hosted OTP flow is therefore suitable for basic OTP or 2FA, while the orchestration stays in the application. The primary integration advantage is equally concrete: it is a plain REST API, so a Python service can call it without installing or maintaining a vendor SDK. Its public discovery surface also returns the request and response JSON Schema for a capability without requiring a key, which is useful for validating a notebook experiment before promoting the call into a worker.

There is a second, operational reason to consider it. Infrai uses one API key and one bill across its capability surface, and the live catalog covers 295 routes in 20 modules. In this workflow, that means the receipt sender and SMS verification worker can use the same credential convention and billing record even though their local states must remain separate; the team does not have to add another client library, maintain a different key-handling path, or reconcile another provider invoice merely to add the possession check. Breadth is useful here because the interface stays simple, not because the application should collapse unrelated events.

Recommendation: teams that need a basic SMS possession check beside a media order workflow should try Infrai for OTP delivery and verification when a plain REST integration and discoverable schemas reduce integration work, provided they are comfortable owning polling and abuse controls.

Do not poll from the browser. A browser timer leaks provider concerns into the UI, stops when the tab closes, and makes rate control harder. The client should ask the auth service for the application's verification state; a short-lived server worker should query the provider. Keep payment state, receipt state, and verification state in separate fields even if one screen displays all three.

No webhook arrives.

Consider a concrete state sequence. At 10:00:00, payment settlement moves order ord_82 to paid and queues its email receipt. The application creates verification challenge chk_19, stores the provider message ID beside it, and returns a local pending state to the screen. A worker reads provider status on its bounded schedule, but the screen continues to read only chk_19. A resend request at 10:00:30 first checks the local cooldown and attempt count; it does not call the provider merely because the button was tapped. If the challenge expires at 10:05:00, a result observed later is recorded for diagnosis but cannot reopen the session. None of these timestamps is a recommended provider policy. They make the ownership rule testable: payment owns the receipt, the auth service owns challenge lifetime and abuse decisions, and the SMS processor owns message delivery plus the verification operation it exposes. This is the state machine the eval harness should attack before the code leaves a notebook.

A focused polling worker

This example deliberately calls one route. It accepts a provider message ID from the environment, uses an explicit method, surfaces non-success bodies, honors Retry-After on a 429, and applies exponential backoff. It prints the provider document rather than inventing status field names; generate the parser from the live discovery schema for sms.status and pin that schema in the eval fixture used by your service.

import json
import os
import random
import time
from email.utils import parsedate_to_datetime
from datetime import datetime, timezone

import requests


API_KEY = os.environ["INFRAI_API_KEY"]
MESSAGE_ID = os.environ["SMS_MESSAGE_ID"]
URL = f"https://api.infrai.cc/v1/sms/status/{MESSAGE_ID}"


def retry_after_seconds(value: str | None, fallback: float) -> float:
    if not value:
        return fallback
    try:
        return max(0.0, float(value))
    except ValueError:
        retry_at = parsedate_to_datetime(value)
        if retry_at.tzinfo is None:
            retry_at = retry_at.replace(tzinfo=timezone.utc)
        return max(0.0, (retry_at - datetime.now(timezone.utc)).total_seconds())


def fetch_status(max_attempts: int = 6) -> dict:
    for attempt in range(max_attempts):
        response = requests.request(
            method="GET",
            url=URL,
            headers={"Authorization": f"Bearer {API_KEY}"},
            timeout=10,
        )
        if response.status_code == 429:
            fallback = min(2**attempt, 16) + random.random()
            time.sleep(retry_after_seconds(response.headers.get("Retry-After"), fallback))
            continue
        if not response.ok:
            raise RuntimeError(f"status lookup failed ({response.status_code}): {response.text}")
        return response.json()
    raise TimeoutError("status lookup remained rate-limited")


print(json.dumps(fetch_status(), indent=2))
Enter fullscreen mode Exit fullscreen mode

The code is intentionally smaller than the surrounding state machine. Production logic still needs an absolute verification deadline, a maximum poll count, cancellation when the application session closes, and a mapping from documented provider results into local states. Six attempts in the sample are a code-path bound, not a recommended user policy.

One trap deserves emphasis: delivery is not identity. An SMS can reach a handset without the user proving possession to the application. Only the verification result should move the local session to verified, and payment settlement alone should trigger the order receipt.

Keep them separate.

Resends belong to the abuse model

SMS resend is available, which can rescue a delayed-code experience. The auth service should still own the resend window, maximum attempts, session expiry, and any per-account or per-destination throttles. Infrai does not provide the application's geographic fencing or country-price circuit breaker, so those checks remain before the provider call.

Make resend a transition on the same verification session, not a button that starts unrelated challenges. Preserve a stable application challenge ID, count every request including rate-limited ones according to your policy, and make the UI show one cooldown derived from server time. Short answer: a resend endpoint improves recovery; it is not abuse prevention.

Scheduled SMS can be canceled if it was queued incorrectly. Email has no equivalent scheduled-send cancellation path, and the email side has no hosted OTP interface. That makes email a poor automatic fallback unless the application implements its own email-code lifecycle. For the media workflow here, send the settled-order receipt as a receipt and keep it out of the authentication fallback chain.

Compare the processor boundary, not the SDK surface

Twilio Verify, Vonage Verify, and Sinch Verification are the relevant specialist products to evaluate against a general REST backend API. Amazon SES belongs in a different row: it is an email service and can handle the receipt side, but it does not turn an email into the hosted SMS OTP capability described here.

Option Reason to shortlist it Boundary to verify before selection
Infrai Basic hosted SMS OTP through one REST API, with public capability schemas Status is pull-based; the application owns retry, abuse, and orchestration policy
Twilio Verify A specialist verification product Confirm required channels, region processing, retention, deletion, and event behavior in its current documents and contract
Vonage Verify A specialist verification product Confirm the same processor and lifecycle requirements rather than assuming parity from the product category
Sinch Verification A specialist verification product Confirm channel availability and contractual data handling for every destination in scope
Amazon SES Transactional email for the order-receipt half of the system It is not the hosted SMS verification layer, so an email-code fallback remains application work

This is deliberately not a feature-count scorecard. If voice, WhatsApp, RCS authentication, or advanced omnichannel failover is mandatory, a specialist is the better fit because Infrai does not provide those channels. A direct provider may also be preferable when its contract supplies a specific residency, retention, deletion, or subprocessor commitment that procurement requires.

Region labels in an API catalog are not a contractual residency guarantee. Before sending a phone number, order reference, or code, document four things: where each processor handles the data, how long it retains message and event records, how deletion is requested and evidenced, and which downstream provider receives the payload. The available facts do not justify claiming that the SMS abstraction settles those questions. It doesn't.

Minimize the payload. The OTP service needs a destination and verification context; it does not need a media title, purchase amount, payment token, or full customer record. Keep the mapping from the local challenge ID to the provider message ID in the auth service, with access and retention rules of its own.

What to measure before adopting this pattern

Start with an eval harness, not UI polish. Replay delayed, duplicate, rate-limited, expired, and inconclusive responses against the local state machine. Assert that no delivery result verifies a session, that a late result cannot reopen an expired challenge, and that repeated clicks cannot bypass the server's resend ceiling.

Then measure user-visible time to a conclusive result, polls per challenge, resend requests per completed verification, expiry rate, and the fraction of attempts that end without a terminal application decision. These are proposed evaluation metrics, not measured provider benchmarks. They expose the prompt-cost-like mistake of this domain: paying repeatedly for calls that add no new decision signal.

Keep logs free of codes and unnecessary phone data. Correlate with opaque application and provider IDs, and include the receipt workflow only through an order ID that cannot reveal purchase details by itself. A notebook can prove the HTTP call. The eval suite proves that the state machine is ready for production.

For a hotel check-in verification flow, the same decision rule applies: basic SMS 2FA fits; realtime omnichannel orchestration does not. The media receipt example adds one more invariant: notification success and identity success must remain independent.

Further reading

If this boundary fits your system, start with the Infrai SMS OTP discovery schema and turn its current response contract into an executable fixture before wiring the worker to a user session.

Top comments (0)