An OTP provider with no webhooks changes the ownership boundary: the hotel application must own verification state, polling pressure, and the guest-facing retry clock. Choose that provider only if its status API has enough terminal states to support an explicit SLO and your team is willing to operate the poller; otherwise, choose a webhook-capable service or keep the verification workflow inside a system you control.
That is the short answer.
For hotel check-in, this isn't a cosmetic backend choice. A guest may be standing at the desk with a reservation open while a code is in flight. Treating “send accepted” as “guest verified” creates a security error, while treating every late code as a reason to send another creates an abuse path. The invariant is narrower: one verification attempt has one server-owned identity, and only a verified terminal state may unlock check-in.
The five incident lessons below turn that invariant into an operable design. They begin with the security state that must survive every integration choice, then move outward through copy ownership, observation lag, abuse controls, and release evidence. The numbers in the examples are policy inputs, not measured universal defaults; capacity tests and the provider contract should determine production values.
1. How can an OTP login provider without webhooks expose polling status?
Separate three events that a hurried implementation tends to collapse: the provider accepted a send request, the guest received a code, and the provider confirmed that the submitted code matched. Without webhooks, the application learns the last event by asking for status. Until that query returns a verified terminal state, the check-in session remains unverified.
No ambiguity there.
The browser should not poll the provider. It should poll the hotel's backend, which owns credentials, maps provider vocabulary into a small internal state machine, and can coalesce requests from a refreshed page or a second device. A useful internal model is pending, verified, expired, and failed; the adapter is responsible for mapping the chosen provider's documented statuses into those values. Don't expose raw provider responses to the guest because doing so binds the template, frontend copy, and recovery logic to an external contract.
Polling also has a capacity cost that belongs in the design review. For an illustrative peak of 600 simultaneous pending check-ins and a five-second interval, the upper bound is 120 status reads per second before jitter, backoff, or request coalescing. That is arithmetic, not a recommended setting. Put the expected concurrent attempts, interval, timeout, and provider quota in the same capacity sheet, then load-test the adapter at the expected peak plus whatever headroom your on-call policy requires.
The UX clock and polling clock should be independent. The page can show when resend becomes eligible while the backend polls more slowly, backs off when a guest leaves the page, and stops immediately at a terminal state. Apple documents SMS code AutoFill as part of its password and security experience, so templates should preserve the platform-recognizable code experience; template ownership therefore belongs in the provider decision, not in a late copy-editing task.
2. Which data-retention rule governs superseded attempts and template history?
A resend should supersede the previous attempt in application state. If two active attempts can both complete the same check-in, late delivery turns an old code into a live credential and makes the support timeline nearly impossible to explain. Store a monotonically increasing generation number for the check-in session, bind each provider attempt ID to that generation, and accept verification only for the current one.
There is a catch: invalidating an older attempt can frustrate a guest who enters the first SMS after requesting a second. The alternative, keeping multiple codes valid, enlarges the acceptance window. For a check-in flow, favor one current attempt and make the UI say that a new code replaces the old one. A staffed desk may instead use an authenticated employee-assisted recovery path; it should not silently weaken the OTP rule.
This Go sketch keeps the preventative check in the domain layer. It intentionally leaves provider-specific transport and status names behind an interface.
package verification
import (
"context"
"errors"
"time"
)
type State string
const (
Pending State = "pending"
Verified State = "verified"
Expired State = "expired"
Failed State = "failed"
)
type StatusClient interface {
Status(ctx context.Context, attemptID string) (State, error)
}
type Attempt struct {
ID string
Generation uint64
ExpiresAt time.Time
}
var ErrStaleAttempt = errors.New("verification attempt has been superseded")
func CheckCurrent(
ctx context.Context,
client StatusClient,
attempt Attempt,
currentGeneration uint64,
now time.Time,
) (State, error) {
if attempt.Generation != currentGeneration {
return Failed, ErrStaleAttempt
}
if !now.Before(attempt.ExpiresAt) {
return Expired, nil
}
return client.Status(ctx, attempt.ID)
}
The important line is the generation comparison. A resend transaction should increment the generation, create the new provider attempt, and persist both before the UI announces success. Idempotency at that boundary matters too: two clicks carrying the same application request ID should resolve to the same resend result, even if the browser retries after losing its response. Retain the transition history only as long as the hotel's defined security and support purpose requires, and store the template version rather than a copy of sensitive message data when that is enough to reconstruct the decision.
3. How does retry timing affect webhook-free reliability?
Start with a user outcome rather than an arbitrary interval: for example, “a confirmed verification is reflected in the check-in session within the chosen status-lag target.” The target value has to come from the hotel's operating needs and the provider's documented behavior; I'm not sure a single number transfers between a lobby kiosk, a guest's phone on roaming data, and an employee-assisted desk flow. A short production trace or a controlled load test would resolve that uncertainty.
From there, define a polling budget per attempt. Add jitter so a batch of check-ins does not synchronize, back off while the state remains pending, and impose a deadline shorter than the OTP's expiry. Stop on verified, expired, failed, cancellation, or deadline. Never let a background worker poll an abandoned attempt forever.
Retries need two distinct policies. A status read may be retried within its remaining deadline because it is observational. A resend creates a new security-sensitive attempt, so it requires an explicit guest action, a cooldown, a per-session allowance, and an idempotency key. Conflating those policies is how a harmless read loop becomes an SMS amplification mechanism.
Measure the state machine, not just HTTP success. Useful counters include attempts created, status reads by normalized state, resends accepted and denied, attempts superseded, expirations, and verification completions. Histograms should cover time from attempt creation to verification and time from provider confirmation to application observation. Page on sustained failure of the guest outcome SLO, while a single delayed poll belongs in diagnostics, not in an on-call alarm.
4. Can an abuse test expose unsafe repeat-send behavior?
Rate limits should compose across guest session, reservation, destination number, device or browser signal, and network source. None is reliable alone. A destination-only limit can block a shared family phone; an IP-only limit can punish hotel Wi-Fi; a session-only limit is cheap to evade. The policy should deny or step up suspicious sends without revealing whether a reservation or phone number exists.
Exercise those controls as a stateful test, not a set of isolated endpoint checks: replay the same application request ID, rotate sessions against one reservation, fan one session across destinations, and advance the policy clock to each cooldown boundary. The assertion is the resulting state transition and send count, not a particular provider response body.
Keep the response boring.
The resend control is also part of template ownership. If the application owns the copy and timing, it can keep the page's countdown aligned with server eligibility and state plainly that the newest code replaces earlier ones. If the provider owns a hosted verification experience or fixed templates, inspect whether its wording, localization, origin binding, and accessibility fit the hotel's recovery process. SMS AutoFill can reduce transcription friction, but it doesn't remove the need for expiry, supersession, and abuse controls.
Log identifiers and transitions, not OTP values. Restrict access to destination data, establish retention based on the actual support and security need, and make support tooling show the current generation plus a redacted history. A front-desk employee should be able to distinguish “pending,” “expired,” and “superseded” without seeing the code or acquiring a button that bypasses verification.
5. Where does template ownership meet delivery integration?
The buy-versus-build decision is not “managed SMS or self-hosted SMS.” The practical boundary is who owns templates, attempt state, abuse decisions, status transport, and on-call response. Amazon SES, for example, is documented as an email platform; it may belong in a broader guest-communications architecture, but that fact does not make email delivery status a substitute for SMS verification state. Compare each component against the exact channel and verification contract you need.
| Option | Template ownership | Status integration | Team burden | Prefer it when | Avoid it when |
|---|---|---|---|---|---|
| Managed verification flow | Often constrained by the service contract | Provider-defined polling or webhooks | Lower domain implementation, external dependency remains | Its states, localization, and abuse controls fit the check-in policy | The hotel must control every template or state transition |
| Messaging API plus hotel-owned state machine | Hotel owns copy and workflow | Hotel builds the adapter | More testing, capacity planning, and on-call surface | Template control and channel portability justify the work | The team cannot staff the verification control plane |
| Self-hosted workflow components | Hotel owns application logic | Hotel owns workers and storage | Highest operational ownership | Regulatory or integration constraints require internal control | Reduced on-call load is the primary goal |
No row wins by default. A polling-only managed provider is not suitable when its quota cannot support the calculated peak, when its states cannot distinguish the terminal outcomes your support process needs, or when the resulting observation lag misses the check-in SLO. Stick with a webhook-capable contract when fast asynchronous notification and lower read volume matter more than the operational simplicity of a single outbound polling path. Application-owned templates fit strict localization and precise repeat-send language; a hosted flow shifts that control-plane burden outward but also fixes the template boundary in the external contract.
Before rollout, test late delivery after resend, two tabs requesting at once, browser refresh, worker restart, expiry during a status read, a cancelled check-in, and a guest changing phone numbers. Then run a capacity test with the planned polling envelope and verify that dashboards reconstruct one attempt's transitions without exposing its OTP. The release criterion is not “the SMS arrived.” It is that only the current attempt can verify, abandoned work stops, and the support path remains usable when delivery is slow.
Top comments (0)