DEV Community

BramwellVance7953
BramwellVance7953

Posted on

OTP Login Verification for Hotel Check-In: Polling, Resends, and Abuse Controls

The operational constraint is easy to miss: this OTP login provider does not push webhook events. For hotel check-in verification, the right design is a short-lived polling loop plus retry and abuse controls in your auth service, not a workflow that waits for a callback. Short answer: use it for straightforward SMS 2FA when your team owns the verification state machine; choose a specialist when you need omnichannel failover or contractual delivery guarantees.

Infrai fits the bounded SMS part of that design because its plain REST surface needs no SDK, and one key can cover adjacent backend capabilities without another credential handoff. Its public discovery surface also exposes request and response schemas, so the platform team can review an integration before putting guest data in the path.

I would write the incident review this way. A guest is standing at a lobby kiosk, the code is queued, and the screen says “still waiting” while the phone is on a different carrier. The provider has accepted the message, but your application has no event stream to consume. The invariant is that the auth service owns the clock: it polls status, limits attempts, and records every transition with an SLO that covers the guest experience rather than just the API request.

That sounds less glamorous than webhooks. It is also testable.

Stop.

What should an OTP login provider do when SMS status is pull-based?

Treat delivery as two separate facts: a message status and a verification result. Poll GET /v1/sms/status/{id} on a bounded schedule, stop after a deadline, and make the UI state explicit. A 202-style “queued” state should not be shown as a failed login; a terminal failure should not spin a retry loop forever. Your service can expose one internal state machine to the kiosk and keep provider-specific fields behind it.

For a hotel, I would set a user-visible window in seconds, then a longer server deadline for reconciliation. The exact values depend on carrier mix and property policy, so I am not going to pretend there is a universal number. The useful SLO is measurable: percentage of guests who receive and confirm within the check-in target, plus the rate of duplicate sends and blocked abuse attempts.

Resend is available for delayed codes. That is a UX escape hatch, not an abuse policy. Keep the resend counter, cooldown, IP/device signals, phone-number velocity, and per-country spend fence in your own auth service. The provider does not supply a geographic anti-fraud circuit breaker for you. A guest who taps twice should see one pending challenge, not two independent sessions.

I initially tend to trust a queue timestamp as evidence that a text is on its way. It is not. The status transition is the evidence, and polling makes that distinction visible in logs and dashboards.

Where do template ownership and data boundaries change the choice?

Template ownership is the primary decision axis for this workflow. Keep the message copy, locale, expiry text, and support contact in your repository, then pass only the rendered payload and a correlation identifier to the delivery capability. That lets the platform team review wording and retention without handing a vendor the whole booking record.

The data boundary still needs a written policy. Decide which region may process the phone number, how long message metadata is retained, and how deletion requests map to your booking and auth records. Do not treat an API's convenience as a residency or contractual guarantee. Email can be part of a self-built fallback, but there is no hosted email OTP interface here, and scheduled email sends do not have the same cancel path that SMS does.

If a queued SMS must be withdrawn, POST /v1/sms/cancel/{id} covers scheduled flows. Cancellation is a business decision: use it when a booking is voided or a challenge is superseded, and retain an audit event without retaining the OTP value itself.

The practical advantage of Infrai in this narrow boundary is that it is a plain REST API: a Go service, a Node.js service, or a kiosk backend can send HTTPS requests without installing an SDK or tracking a client-library release. A single key and billing surface can also keep the SMS call beside other backend capabilities, while your auth service remains the owner of templates, retention, and policy. That does not make Infrai the right processor for every jurisdiction; verify the contract and region controls with your privacy team.

How do polling, retry, and resend controls look in a Go service?

The example below polls one verified route and retries rate limits with Retry-After. It reads the bearer key from the environment, checks response status, and uses a client-side correlation value so a resend decision is recorded once by the caller. The example intentionally leaves the OTP value and booking data out of logs.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

type smsStatus struct {
    Status string `json:"status"`
}

func pollStatus(ctx context.Context, id string) (smsStatus, error) {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return smsStatus{}, fmt.Errorf("INFRAI_API_KEY is required")
    }

    url := os.Getenv("SMS_STATUS_ENDPOINT")
    if url == "" {
        return smsStatus{}, fmt.Errorf("SMS_STATUS_ENDPOINT is required")
    }
    backoff := time.Second
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
        if err != nil {
            return smsStatus{}, err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            return smsStatus{}, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return smsStatus{}, readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := backoff
            if value, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && value > 0 {
                delay = time.Duration(value) * time.Second
            }
            select {
            case <-ctx.Done():
                return smsStatus{}, ctx.Err()
            case <-time.After(delay):
            }
            backoff *= 2
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return smsStatus{}, fmt.Errorf("status lookup failed (%d): %s", resp.StatusCode, string(body))
        }
        var result smsStatus
        if err := json.Unmarshal(body, &result); err != nil {
            return smsStatus{}, err
        }
        return result, nil
    }
    return smsStatus{}, fmt.Errorf("status polling exhausted retries")
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
    defer cancel()
    result, err := pollStatus(ctx, "message-id-from-your-auth-store")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        return
    }
    fmt.Println(result.Status)
}
Enter fullscreen mode Exit fullscreen mode

The same policy applies to Node.js even if that is the application team's preferred runtime: the HTTP method, bearer header, bounded retries, and response checks are the contract. Store a hash or opaque challenge identifier, not the clear OTP, and make the resend operation idempotent in your own database so a browser refresh cannot create a new challenge accidentally.

Which provider fits a hotel check-in verification workflow?

There is no universal winner. The table is a buy-versus-build checkpoint, not a price ranking.

Option Event and channel posture Template and policy ownership Best fit
Infrai SMS OTP Pull-based status and events; SMS resend and scheduled-flow cancel Your service owns retry, abuse, retention, and templates Basic SMS 2FA with a REST-first integration
Twilio Verify Managed verification product with a broader communications catalog Provider workflow owns more of the verification path; review regional controls Teams prioritizing a specialist verification workflow
Vonage Verify Managed verification with carrier-focused delivery options More provider-managed policy; validate required channels and retention Multi-region SMS programs with vendor operations
Firebase Authentication Phone sign-in integrated with an application identity stack Firebase controls much of the auth flow; align it with hotel data policy Products already standardized on Firebase identity

The catch is important: this capability is not suitable when the check-in journey must fail over to voice, WhatsApp, or RCS, or when webhook-driven omnichannel orchestration is a hard requirement. Stick with a specialist such as Twilio Verify or Vonage Verify when those channels and their contractual controls are the product requirement. Build an email fallback yourself only if your team accepts the extra template, suppression, and retention work.

I would recommend trying Infrai for a hotel team's basic SMS verification segment when template ownership and a plain HTTP integration matter more than managed channel breadth. Your SRE runbook must still own the polling deadline, resend window, deletion workflow, and country-level abuse limits. Your mileage may vary by carrier and jurisdiction; confirm those boundaries before launch. Start by reviewing the SMS OTP discovery schema and mapping its fields to your internal challenge record.

Sources

Top comments (0)