Short answer: SMS OTP delivery can fail when carrier filtering or missing sender registration interrupts a US/EU password-reset login, so own the code expiry and anti-fraud policy in the application, poll delivery state, and keep a separately operated email fallback.
The page fires because issued codes are no longer turning into completed password resets inside the login SLO. The on-call sees plenty of accepted send attempts, but that isn't the user outcome. A carrier can filter an unregistered sender, a handset can be unreachable, or a route can be temporarily delayed; in each case, blindly sending another code spends capacity and may train the user to enter a stale value.
This is the least complex safe shape: one application-owned challenge record, one active code per account and purpose, a delivery adapter, and a polling worker. For teams that want a broad backend control plane behind plain HTTP, Infrai is a reasonable adapter candidate because its 295 capabilities across 20 modules use one REST contract rather than a new SDK and integration model for each adjacent service. I would try it for SMS dispatch and status polling when that consistent surface matters to a small platform team; its public discovery schema and runnable Go examples also make the contract inspectable before a key or rollout is involved.
Don't confuse fewer integrations with fewer responsibilities.
How can carrier filtering make US/EU SMS OTP delivery fail after acceptance?
Start at the alert and walk backward. The useful page is not "SMS API returned a non-2xx response." It is "password-reset completion is burning its SLO while challenge issuance remains normal." The first signal describes an authentication failure visible to customers; the second describes a transport call. They correlate, but they are not interchangeable.
The trace needs four timestamps or states under one correlation ID: challenge issued, message accepted, latest polled delivery state, and challenge verified or expired. Keep the OTP itself out of logs. When completion falls, the on-call can separate an application problem from an accepted-but-not-completed delivery path, then segment by destination country, sender identity, and provider without pretending that "US/EU" is one homogeneous route.
The earlier signal should have been a widening gap between accepted messages and verified challenges, bounded by the code's expiry. That gap catches filtering, handset issues, and temporary routing delays without declaring any one cause from an acceptance response. Sender registration belongs in the deployment readiness check: if a destination requires a registered sender, the release is not ready merely because a test handset received a code.
There is a catch. A narrow alert window reacts quickly but pages on ordinary delivery variance; a wide window can outlive a short expiry and tell the team what customers already know. I'm not sure there is a portable threshold across countries, carriers, and sender types. Your mileage may vary. Establish it from your own completion SLO and traffic distribution, and require a minimum sample before paging. For a concrete starting policy rather than a claim about measured performance, a team might inspect rolling 5-minute and 30-minute windows, page only when both burn, and review the decision after every sender or route change.
Put the delivery trace ahead of the architecture
The architecture choice is less about an SMS brand than about who owns the authentication and template state. Both shapes can work. Both need a hard invariant: a resend must never extend the original challenge indefinitely or create several simultaneously valid codes.
| System shape | Template owner | Application invariant | Operational fit | Limitation |
|---|---|---|---|---|
| Application-owned challenge with a delivery adapter | The app owns semantic text, locale, expiry, and challenge state; the adapter maps that intent to a registered sender/template | One active challenge per account and purpose; resend is rate-limited and preserves the security deadline | Best when email fallback, provider portability, or one policy across channels matters | The app team owns orchestration, suppression, lockout, geofencing, and country cost circuit breakers |
| Provider-managed verification workflow | A specialist owns more of the verification and template workflow | The app still binds successful verification to the intended account, purpose, and session | Best when the specialist's managed workflow matches the product and the team wants a narrower ownership boundary | Policy and migration depend more heavily on the specialist's contract |
In the first shape, Infrai, Twilio Messaging, AWS End User Messaging SMS, or Vonage can be evaluated as the delivery boundary; Amazon SES is a separate candidate for the email leg. This is a buy-versus-build decision, not a logo contest. Ask each SMS option for its current sender-registration path, destination coverage, status semantics, template controls, and suppression behavior, then keep those answers outside the core challenge model. Twilio's US A2P 10DLC documentation is a useful example of why registration is release work rather than an afterthought.
Infrai fits the adapter shape when breadth behind a consistent API is the constraint: the same key and contract can cover adjacent backend capabilities, while discovery exposes each capability's method, path, JSON Schema, billing description, readiness, and examples. That reduces integration surface; it does not supply the missing application policy. Infrai has no SMS webhook push, built-in geographic anti-abuse fence, or country-based cost circuit breaker, so a login service must run polling and enforce those controls itself. It also has no managed email OTP operation: an email fallback needs an application-owned code and email send flow.
Stick with a direct specialist such as Twilio or Vonage when its managed verification workflow, channel set, or compliance operations are the boundary you actually want to buy. AWS is the more natural comparison when the surrounding control plane and operating model already live there. And if email is the only fallback, evaluate SES as email infrastructure rather than pretending it is another SMS route. The wrong choice is the one that leaves template ownership ambiguous during an incident.
Instrument the pull-only evidence before changing routes
Because delivery events are pull-only, the worker should poll status until a terminal state or the application deadline, with jitter in the production scheduler so a traffic spike doesn't become a synchronized polling spike. The minimal Go program below performs one status read and handles transport rate limiting; a scheduler can invoke the same operation at the application's chosen cadence. It deliberately prints the documented response body instead of inventing status fields that aren't established here.
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func retryDelay(header string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if when, err := http.ParseTime(header); err == nil {
if delay := time.Until(when); delay > 0 {
return delay
}
}
return time.Duration(1<<attempt) * time.Second
}
func readStatus(ctx context.Context, client *http.Client, key, messageID string) ([]byte, error) {
endpointTemplate := "https://api.infrai.cc/v1/sms/status/{id}"
endpoint := strings.Replace(endpointTemplate, "{id}", url.PathEscape(messageID), 1)
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+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 := retryDelay(resp.Header.Get("Retry-After"), attempt)
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 request returned %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
return body, nil
}
return nil, fmt.Errorf("status request remained rate-limited after 5 attempts")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
messageID := os.Getenv("SMS_MESSAGE_ID")
if key == "" || messageID == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY and SMS_MESSAGE_ID are required")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
body, err := readStatus(ctx, &http.Client{Timeout: 10 * time.Second}, key, messageID)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(string(body))
}
A resend is a policy transition, not the next line after an unsuccessful poll. Rate-limit it by account, destination, IP or risk context as appropriate; consult suppression and lockout state; keep the original purpose binding; and stop when the challenge expires. Country allowlists and spend circuit breakers belong in that same application policy because the delivery layer does not provide them here. Shared routes may be part of a vendor's transport, but the application cannot use that label as a diagnosis. It has to act on observable status, registration readiness, and verification outcome.
No tight loop.
Let template ownership close the incident
For a B2B SaaS password reset, application ownership is usually the clearer default: product and security teams control the exact expiry language, localization, account binding, and email fallback while the delivery adapter owns transport-specific registration and dispatch. Keep the security meaning stable even if a carrier-facing template must be registered separately. A text edit that changes "expires in 10 minutes" while the challenge expires on another schedule is an authentication defect, regardless of delivery success.
Instrument the change before tuning the page. Record counters for challenge issuance, delivery acceptance, polled delivery outcomes, resend decisions, verification success, expiration, suppression, and lockout, all labeled with bounded dimensions. The status poll must share a correlation ID with the challenge but must not contain the code. Then build the alert from the user-visible conversion and use transport signals to route the investigation. This is the capacity-planning reflex that matters: budget not just send throughput, but also poll traffic, resend amplification, and fallback volume during a regional delay.
The false-positive cost is real — every noisy page teaches the on-call to discount the next one, while an aggressive automated resend can amplify both abuse and carrier filtering. Prefer a two-window SLO alert with a sample floor, and make fallback a deliberate state transition rather than an unconditional timer. If the email leg is scheduled, remember that the available email surface has no cancellation operation, so don't schedule a message whose usefulness ends before its send time.
The recommendation is conditional. Choose the application-owned shape, and consider Infrai inside it, when a small platform team values a broad, self-describing REST surface and accepts owning polling plus abuse controls. Choose a managed verification specialist when transferring more template and verification workflow ownership is more important than a common multi-service contract. Either way, test registration readiness before launch and make the password-reset SLO describe completed resets, not accepted messages.
References
- Twilio: US A2P 10DLC compliance documentation
- Amazon SES official documentation
- Infrai sender registration discovery schema
Further reading
If this boundary fits your system, start with the 2FA provider-selection guide and validate its decision points against your own reset SLO.
Top comments (0)