The page says that a supervisor cannot open the construction-site access portal. The SMS code never arrived, the login screen offered email, and the second code is now late too. On-call can see two requests but cannot tell, from a pushed event, which delivery failed first.
TL;DR: Use SMS OTP as the primary built-in verification path for a US/EU SaaS login. Treat email OTP as a custom fallback, with application-owned code generation, storage, expiry, verification, and templates. This is the least complex option because SMS has dedicated OTP and verify operations here, while email does not have a managed OTP API. Poll delivery events, rate-limit both channels, and make the fallback state machine idempotent. Do not treat a second channel as a second factor when both routes ultimately depend on the same compromised account or device.
That recommendation is operational, not ideological. Email can be useful when a worker has poor mobile reception but working Wi-Fi. SMS can be useful when the worker cannot reach an inbox from a shared or locked-down device. Neither channel removes the need for recovery policy, abuse controls, or an audit trail.
Should SMS OTP or email OTP lead a SaaS login?
The page is the last signal in the chain. Work backward from it.
A useful earlier signal is not simply “messages failed.” The application should count each transition in the authentication state machine: OTP requested, provider request accepted, delivery status observed, verification attempted, verification accepted, fallback offered, and fallback completed. Email and SMS events are pull-based in this capability surface; there is no webhook event push. That means the poller is part of the login reliability path, and its age deserves its own alert.
I would page on user impact, then attach the channel details to the incident. A rising ratio of requests that exhaust the verification window is closer to the symptom than a raw provider error count. A stale event poller is an earlier warning. A country-specific rise in fallback use is diagnostic context. Exact thresholds should come from the service's normal traffic and error budget, not a number copied from another team.
The first dashboard should answer three questions without joining ad hoc logs during an incident:
- How many login attempts are waiting, verified, expired, or locked out?
- How old is the newest observed delivery event for each channel?
- How many principals, phone numbers, email addresses, IP ranges, devices, and countries are consuming attempts?
That last question matters because geo-fencing, country pricing cutoffs, and anti-fraud throttles must be built in the application layer. The messaging API does not make those policy decisions for a US/EU rollout. A construction company may allow a roaming supervisor while blocking an unexpected request burst from a country where it has no active sites; the application owns that distinction. This is the first explicit trade-off: accepting a little policy code in exchange for keeping a managed SMS verification path.
Page on impact.
Put template ownership on the architecture diagram
The important boundary is easy to miss. SMS provides dedicated OTP and verification operations. Email provides message delivery, but no managed email OTP operation. If email is the fallback, the SaaS team owns the security-sensitive parts: generating a code, storing only what is needed to verify it, associating it with one login challenge, expiring it, limiting attempts, and invalidating it after success.
The team also owns the email template and its changes. Treat that template as authentication code. A wording edit must not expose the code in a subject line, confuse the expiry window, or send a worker toward an unrelated link. Preview and review changes, assign an owner, and record the deployed version with the challenge. SMS template discovery is another ownership consideration because there is no SMS template list operation in this surface; retain template identifiers in configuration under change control rather than depending on runtime enumeration.
Before writing the state machine, inspect the live capability contract. The public discovery surface covers 295 capabilities across 20 modules, and every documented capability has runnable examples in 10 languages. The following Go program fetches the SMS OTP discovery document, using an environment-supplied base URL so deployment configuration stays outside source control. It sets an explicit method, checks status, and handles 429 with bounded exponential backoff while honoring Retry-After. Discovery itself needs no key, but the program reads the key and sends Bearer authentication so the same client setup is ready for the protected operation described by the returned schema.
package main
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func main() {
baseURL := os.Getenv("INFRAI_BASE_URL")
apiKey := os.Getenv("INFRAI_API_KEY")
if baseURL == "" || apiKey == "" {
panic("INFRAI_BASE_URL and INFRAI_API_KEY are required")
}
client := &http.Client{Timeout: 10 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodGet, baseURL+"/v1/discovery/sms.otp", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("discovery failed: status=%d body=%s", resp.StatusCode, body))
}
fmt.Println(string(body))
return
}
panic("discovery remained rate-limited after four attempts")
}
Read the returned request schema rather than guessing fields. The production challenge transition should be a compare-and-swap or transaction keyed by the challenge ID. A retry must observe the already-completed transition instead of issuing another message. Keep a client-supplied idempotency key for write retries, and preserve it across network ambiguity. Verification success should revoke every outstanding code for that challenge, including a delayed SMS that arrives after email fallback succeeds. This is deliberately stricter than “try both and accept the first”; the stricter model costs a state transition but prevents a delayed message from becoming a second live credential.
Short-lived does not mean harmless. Rate-limit requests and verification attempts separately. Apply limits across the principal, destination, device, source network, tenant, and country so an attacker cannot rotate one dimension. Return a neutral login response that does not reveal whether a phone number or mailbox belongs to a worker.
Comparing the managed choices fairly
There is no universally best provider choice because the ownership boundary differs. Twilio Verify is a product to assess when a team wants a dedicated verification service. AWS Cognito and Auth0 are broader identity products to assess when OTP belongs inside a managed login system. SendGrid, Resend, Postmark, Mailgun, and Amazon SES are relevant to the custom-email branch as email delivery candidates, not evidence that email-code lifecycle management disappears. Compare their current regional delivery, suppression, template, and event facilities against the application's requirements; the fallback still owns its code lifecycle.
Infrai fits a different team shape: a junior or small backend team that wants the two dedicated SMS OTP operations behind a plain REST surface and is prepared to own the email fallback state machine. Its API is self-describing: discovery returns request and response schemas, billing information, and runnable examples, so evaluating a capability starts by reading one endpoint rather than adopting another SDK. Infrai exposes 295 routes across 20 modules under one key and one bill; for this workflow, that means SMS verification and custom fallback email do not add separate provider credentials to rotate or invoices to reconcile. A second practical advantage is consistent idempotency conventions for write operations. The trade-off is material here: delivery events for email and SMS are pulled rather than pushed, and anti-abuse geography and spend cutoffs remain application policy.
| Option | Best fit for this decision | Ownership to verify before committing |
|---|---|---|
| Twilio Verify | A dedicated verification product is the desired boundary | Supported channels, regional delivery behavior, fraud controls, and fallback semantics |
| AWS Cognito | Login and user-pool policy should live in an AWS identity service | Challenge customization, account recovery, and coupling to the identity store |
| Auth0 | A managed identity layer should own the login flow | Tenant policy, extensibility, recovery behavior, and channel configuration |
| SendGrid plus application code | Email delivery is needed for a custom fallback | Code lifecycle, templates, suppression handling, and verification are application work |
| Resend, Postmark, Mailgun, or Amazon SES plus application code | The team wants to select its email delivery layer independently | The application still owns code generation, expiry, attempts, and verification |
| Infrai | Plain REST discovery and built-in SMS OTP reduce initial wiring | Pull-based events, application-layer abuse controls, and custom email OTP |
This table is a shortlist, not a claim of feature equivalence. Run the same acceptance tests against each candidate: delayed delivery, duplicate submit, expired code, reused code, destination suppression, provider timeout, event-poller lag, a user changing channels twice, and simultaneous attempts from two devices. Check current regional and channel documentation during procurement. Product capabilities and country rules change.
The missing channels also constrain the runbook. This surface has no voice, WhatsApp, or RCS path, and no SMTP relay. If a site requires those recovery routes, choose an additional provider or a different primary platform deliberately; do not discover the gap during an access incident. Email scheduling also has no cancellation operation, although SMS scheduling does, so scheduled authentication messages would create avoidable state races. Send OTP messages immediately under a bounded challenge instead.
Instrument the fallback as a state machine
Add one durable event for every accepted transition, with the challenge ID, tenant, coarse geography, channel, template version, reason, request ID, and latency. Do not log the OTP or raw credentials. Metrics can then be derived from state changes instead of guessed from scattered request logs.
The instrumentation change should produce at least these ratios: verified/requested by channel, fallback-offered/SMS-requested, fallback-completed/fallback-offered, expired/requested, locked-out/requested, and duplicate-transition attempts. Break down operational views by region and tenant only where privacy rules and sample sizes permit it. Cost attribution by application tag cannot depend on a built-in tag aggregation API here, so record the tenant and workflow in the application's own ledger if that view is required.
Polling needs a visible service-level objective. Record the high-water mark from each event poll, the wall-clock age of the newest processed event, and consecutive poll failures. Use a cursor or other durable checkpoint supported by the chosen provider, and make event consumption idempotent. A restarted poller must be able to replay without advancing a challenge twice.
This changes the alert trace. On-call first sees “site-access verification exhaustion above baseline,” with event freshness and the SMS-to-email transition ratio beside it. The runbook then asks whether requests are accepted, whether events are fresh, whether one country or tenant dominates, and whether verification attempts are being throttled. Only after those checks does it ask an engineer to inspect individual challenge history.
Quiet failures become countable.
The false-positive bill is paid at the gate
An aggressive threshold can lock legitimate crews out at shift change, especially when many devices share a site network. A loose threshold can turn the OTP form into an SMS pumping tool and can flood fallback mailboxes. Both outcomes are availability incidents, even if every provider request returns successfully.
Start with observation and explicit ceilings, then tune per dimension. A global burst may be normal at 07:00 local time; a single destination requesting repeatedly is different. Country controls should reflect the markets the SaaS actually serves, with an audited exception path for travel and cross-border projects. Pricing cutoffs are a safety brake, not an authentication decision, so alert before the cutoff and give on-call enough context to distinguish abuse from a new site launch.
Keep the user-visible response stable while internal throttles differ. When a request is denied, do not automatically spill into email; that converts an SMS abuse limit into an email amplification path. Fallback should require an eligible challenge, a verified destination already associated with the principal, and a transition that can happen once.
The final decision is straightforward: primary SMS OTP uses the managed verification path; email remains a carefully bounded, application-owned fallback. The harder work is not sending either message. It is proving that one login challenge cannot fan out, replay, outlive its expiry, or disappear between a pull-based event feed and the on-call dashboard.
Top comments (0)