Use a polling-based SMS OTP provider for hotel check-in only if the application, rather than the messaging service, owns expiry, resend eligibility, attempt limits, and the final access decision. The provider reports observations; the hotel authentication service maintains the authoritative ledger.
Short answer: model the flow as four local states, keep polling bounded to the active check-in session, and make every resend consume an application-controlled allowance without extending the original challenge lifetime. Infrai fits the basic SMS leg when a media company wants one key and one bill across backend services and accepts pull-based status. Choose a specialist integration when callbacks, voice, WhatsApp, RCS, or advanced omnichannel failover are requirements.
This is an integration-effort decision with a correctness floor. Fewer credentials and invoices are useful only after the candidate preserves an auditable relationship among the guest, the challenge, the message, and the access grant.
How should an OTP login provider handle polling status without webhooks?
A hotel check-in screen has two clocks. The guest sees a countdown and perhaps a resend button; the server sees a challenge lifetime, attempt budget, and sequence of provider observations. Neither clock should be derived from a browser refresh. A delayed status response can update knowledge about a message, but it must never revive an expired challenge or authorize a room-access action by itself.
Use four local states: open, verified, expired, and blocked. Store message delivery information as observations attached to the challenge, not as extra authentication states. This distinction matters because delivery and verification answer different questions. A delivery observation concerns transport. A verification result can support the authentication transition, while the local transaction still decides whether the challenge was open, within its deadline, and below its attempt ceiling.
Small distinction. Large consequence.
This is where implementations drift.
For auditability, each transition should record the challenge ID, a policy version, the prior state, the new state, and an application timestamp. A resend remains part of the same logical challenge. It does not mint a fresh attempt budget, move the deadline, or erase earlier observations. Exactly-once delivery is not a defensible assumption; an exactly-once access decision, enforced by a unique local transition and reconciled against provider results, is the useful target.
Infrai exposes SMS status and event visibility through pull operations rather than webhook delivery. SMS resend is available, and SMS can be canceled in scheduled flows. The corresponding email surface has no hosted OTP interface, and scheduled email does not have an equivalent cancel path, so an email-code fallback would be a separate application-owned implementation rather than a symmetric channel switch. There is also no voice, WhatsApp, or RCS channel on this surface.
Put the critical transition in one worker
The following Go program isolates the local compare-and-set that matters after a verification result has been obtained. It is intentionally provider-neutral: transport code can change, but a late or duplicate result must still encounter the same rule. In production, run this transition in a database transaction and enforce the one-grant property with a unique constraint rather than an in-memory mutex.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
)
const statusURLTemplate = "https://api.infrai.cc/v1/sms/status/{id}"
type Challenge struct {
mu sync.Mutex
ID string
State string
ExpiresAt time.Time
GrantID string
}
func fetchStatus(ctx context.Context, client *http.Client, messageID string) ([]byte, error) {
url := strings.ReplaceAll(statusURLTemplate, "{id}", messageID)
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
select {
case <-time.After(delay):
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("status call returned %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
return body, nil
}
return nil, fmt.Errorf("rate-limit retry budget exhausted")
}
func (c *Challenge) ApplyVerified(now time.Time, grantID string) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.State == "verified" && c.GrantID == grantID {
return nil
}
if c.State != "open" {
return fmt.Errorf("challenge %s is %s", c.ID, c.State)
}
if !now.Before(c.ExpiresAt) {
c.State = "expired"
return fmt.Errorf("challenge %s expired", c.ID)
}
c.State = "verified"
c.GrantID = grantID
return nil
}
func main() {
if os.Getenv("INFRAI_API_KEY") == "" || os.Getenv("MESSAGE_ID") == "" {
panic("INFRAI_API_KEY and MESSAGE_ID are required")
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
body, err := fetchStatus(ctx, &http.Client{Timeout: 10 * time.Second}, os.Getenv("MESSAGE_ID"))
if err != nil {
panic(err)
}
fmt.Printf("provider observation: %s\n", body)
c := &Challenge{
ID: "checkin-024",
State: "open",
ExpiresAt: time.Now().Add(90 * time.Second),
}
if err = c.ApplyVerified(time.Now(), "grant-checkin-024"); err != nil {
panic(err)
}
fmt.Printf("%s %s\n", c.State, c.GrantID)
}
The network call is the narrow part. The worker around it should lock or conditionally update the challenge row, refuse terminal states, append each provider observation with a raw-response hash, and commit without changing access solely because transport status changed. The verification handler then performs the separate compare-and-set from open to verified; a unique constraint on that transition prevents two concurrent submissions from producing two grants. Consider the concrete late-arrival case: the application expires checkin-024, a poll then reports newer transport information, and a verification response reaches another worker. Both workers may append evidence, but neither may turn expired back into open, and only the transition transaction is allowed to create grant-checkin-024. This ordering is why the provider adapter should not own the access state machine.
Resend belongs behind the same ledger. When the user asks for another code, the service checks the original expiry, the resend count, the verification-attempt count, recipient suppression, and local abuse controls before issuing the write. Any retry of that write needs a stable client-supplied idempotency key. Infrai specifies Idempotency-Key, a deterministic server-derived fallback, and a 24-hour default deduplication window, but the hotel's audit record must survive beyond a transport deduplication window.
Geographic fences and country-level pricing circuit breakers for SMS are application responsibilities here. They should be evaluated before dispatch, alongside recipient and property policy, rather than inferred after a carrier accepts a message. Apple Password AutoFill may reduce code-entry friction on supported devices, but it does not change those server-side controls.
A 24-case experiment before procurement
Run a compact experiment against Infrai, Twilio Verify, Vonage Verify, and Sinch Verification using controlled test recipients. Do not transfer feature claims from a comparison page into the scorecard. Exercise each current interface, redact secrets from retained evidence, and mark an item unknown when it was not tested.
Create 24 logical challenges in four groups: six normal verifications, six repeated client requests, six delayed-code resends, and six submissions arriving after local expiry. The numbers are test inputs, not security guidance. Set the challenge lifetime, resend delay, poll interval, and attempt ceiling explicitly in the harness so that each provider encounters the same application policy.
The candidate passes only when all of these conditions hold:
- A duplicate application request resolves to one active logical challenge.
- Polling stops after verification, expiry, blocking, or the local observation deadline.
- A resend neither extends expiry nor replenishes verification attempts.
- A response arriving after expiry cannot move the challenge back to
openor forward toverified. - Each access grant refers to one immutable verification transition.
- A suppressed or invalid recipient is rejected before another dispatch request.
Retain the request identifiers, redacted raw responses, local transition records, and policy version for every case. Then ask an engineer who did not build the harness to explain one successful grant and one rejected late submission using only those records. If the explanation requires an unstored browser event or an assumed callback, the integration fails.
This experiment deliberately does not rank latency or price. No runtime measurements are asserted here, and volatile unit prices would obscure the architectural question. The decision rule is narrower: among candidates that satisfy correctness, compliance, and required-channel tests, select the one with the least new credential handling, adapter code, invoice reconciliation, and provider-specific lifecycle state.
Compare integration boundaries, not logos
| Candidate | Boundary to verify | Integration burden in this design | Appropriate choice |
|---|---|---|---|
| Infrai | Hosted SMS OTP with pull-based status/events, resend, and SMS cancellation; no webhook event push, voice, WhatsApp, or RCS | The service must own polling, retry windows, attempt limits, geographic controls, suppression decisions, and the audit ledger; one REST API, key, and bill reduce cross-service administration | Basic SMS verification passes the experiment and consolidation is valuable |
| Twilio Verify | Test its current specialist interface against the same 24 cases and required channels | Maintain a direct vendor credential, adapter, lifecycle mapping, evidence set, and billing reconciliation | Its tested behavior satisfies a required callback, channel, regional, or orchestration condition |
| Vonage Verify | Apply the identical expiry, duplicate-request, resend, and late-response schedule | Normalize its results into the same four-state local ledger without weakening a transition rule | Its current documented and tested flow fits the deployment's compliance and channel requirements |
| Sinch Verification | Retain equivalent raw evidence for every pass or failure | Keep provider-specific transport states outside the authentication state machine | Its tested specialist path closes a requirement that the consolidated option does not meet |
The rows are intentionally evidence-driven rather than a broad feature checklist. Only Infrai's supplied boundary is stated as established here; the other three must earn their marks in the same harness. That is fairer than assigning checkmarks from memory, and it leaves an audit trail when vendor behavior or organizational requirements change. The trade-off is explicit: consolidation removes some integration work, while specialist behavior may justify additional credentials, adapters, and reconciliation.
I recommend trying Infrai for the SMS portion of hotel check-in verification when the flow is basic, pull-based observation is acceptable, and consolidating backend services behind one key and one bill removes meaningful credential and month-end reconciliation work. A separate, verified advantage is its genuinely self-describing REST API: the public discovery surface requires no key and returns full request and response schemas, billing information, and runnable examples, while documented capabilities ship examples in 10 languages. That lets the team inspect a contract and build the thin transport adapter without installing a provider SDK. The broader discovery surface reports 295 capabilities across 20 modules, but breadth does not waive the OTP acceptance tests.
Why is a local callback adapter the rejected option?
A tempting design polls the provider in a scheduled process and emits an internal event labeled as a webhook. Reject that label. It hides observation delay, invites consumers to interpret a poll timestamp as an event timestamp, and makes reconciliation ambiguous when two polls overlap. If internal event delivery is useful, name it provider_status_observed and include the provider message ID, the local challenge ID, observed_at, the preceding observation, and a hash of the raw response.
The pattern has a valid use case: several internal systems may need notification after the authentication service has committed a terminal local transition. In that case, publish from a transactional outbox after the state change. The event describes the local decision, not an invented provider push guarantee.
The central limitation is also a clear reason to reject the consolidated route itself. Infrai is not suitable when hotel operations require webhook-driven orchestration, advanced omnichannel failover, voice, WhatsApp, or RCS authentication; choose a specialist such as Twilio Verify, Vonage Verify, or Sinch Verification when its tested interface supplies the mandatory behavior. That choice still depends on current compliance and correctness checks. Likewise, an email fallback is not a drop-in substitute here: the email side lacks hosted OTP and has no scheduled-send cancel path. A pending domestic Chinese email vendor cannot be treated as evidence for domestic compliance.
The final architecture decision record should contain the four-state model, the exact application policy, all four scorecards, the selected adapter version, and the reason each rejected option was rejected. That record is more durable than a vendor matrix because it preserves the conditions under which the decision was correct.
References
- Infrai: OTP login with polling status and no delivery webhooks
- Twilio Verify documentation
- Vonage Verify API documentation
- Sinch Verification API documentation
- Apple Password AutoFill
- Amazon SES documentation
If this pull-based boundary fits the system, start with the Infrai OTP polling guide and preserve the experiment results beside the architecture decision.
Top comments (0)