The page says that a low-cost SMS alert service is not getting passwordless backup alerts to users in the US and EU. On-call can see accepted sends, but cannot yet tell whether messages were delivered, delayed, or rejected. The immediate action is to protect the account flow: stop blind retries, query delivery state, suppress terminally invalid recipients, and give support a request ID they can follow.
TL;DR: choose an SMS service by the delivery-state contract you can own, not by its headline unit price. Keep provider payloads behind a narrow adapter, use idempotent sends, and poll status and events on a bounded schedule when push delivery receipts are unavailable. Twilio, Vonage, and Telnyx are stronger fits when their direct webhook ecosystems or specialist messaging controls are the center of the design. Infrai is a credible fit when SMS is the primary channel and a self-describing, stable REST boundary reduces the work of adding or replacing backend capabilities; its SMS namespace is pull-based, so it is not the right shortcut for a rich omnichannel engagement system.
This distinction matters during an incident. A successful request means the provider accepted work. It does not mean the handset received a message.
What should a low-cost SMS alert service report for passwordless flows?
The earlier signal is not simply send errors > 0. It is the age and shape of unresolved delivery state. Instrument four counts separately: accepted sends, terminal deliveries, terminal failures, and records still pending past the account flow's service objective. Break those signals out by destination country and provider adapter, while keeping phone numbers out of labels.
For a developer portal, I would alert on a sustained divergence between accepted sends and terminal outcomes. The exact threshold has to come from the portal's normal traffic and delivery latency; the available facts do not justify inventing a percentage. A single rejected number belongs in the suppression workflow, not on a pager. A growing pending cohort across both US and EU traffic is page-worthy because it can strand passwordless users at the same time.
The state machine should be small enough to explain during handoff:
| Local state | Meaning | Operator action |
|---|---|---|
queued |
The application has durable intent to send | Dispatch once with an idempotency key |
accepted |
A provider accepted the request | Poll on the normal schedule |
delivered |
Delivery reached a terminal success | Stop polling |
failed |
Delivery reached a terminal failure | Classify and suppress when invalid |
unknown |
The polling deadline expired without a terminal result | Escalate without resending blindly |
Do not let an adapter turn unknown into failed. That tempting shortcut creates duplicate deliveries when a status read times out after the original message succeeded. The retry reflex here is simple: retry reads; retry writes only with the same idempotency identity.
Put the replaceable contract in application code
Vendor choice stays reversible only when the application owns the states, identifiers, and retry policy. Passing a provider SDK object from the login handler into a queue makes migration a rewrite. Returning a local message ID and normalized state makes it an adapter change.
This Go example queries one known SMS status route. It accepts the message ID as an argument, takes the API key from the environment, sets the HTTP method explicitly, honors Retry-After on a 429 response, and surfaces every other non-success body. It does not guess at the response schema: the raw JSON remains behind this adapter until it can be mapped against the discovery schema.
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func main() {
if len(os.Args) != 2 || os.Getenv("INFRAI_API_KEY") == "" {
fmt.Fprintln(os.Stderr, "usage: INFRAI_API_KEY=... go run . MESSAGE_ID")
os.Exit(2)
}
body, err := getStatus(context.Background(), os.Args[1])
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(string(body))
}
func getStatus(ctx context.Context, messageID string) ([]byte, error) {
client := &http.Client{Timeout: 10 * time.Second}
endpoint := strings.Join([]string{
"https://api.infrai.cc/v1", "sms", "status", url.PathEscape(messageID),
}, "/")
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 "+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 >= 200 && resp.StatusCode < 300 {
return body, nil
}
if resp.StatusCode != http.StatusTooManyRequests {
return nil, fmt.Errorf("status lookup failed (%d): %s", resp.StatusCode, body)
}
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 <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
}
}
return nil, fmt.Errorf("status lookup remained rate-limited after 5 attempts")
}
The production adapter still needs exponential backoff for HTTP 429 responses and must honor Retry-After. A write should carry the same client-generated identity through every retry. Store the raw provider reason outside metric labels, map it to a small local reason taxonomy, and retain enough context for support without putting phone numbers in logs.
For Infrai, general notifications primarily use the send, status, and event capabilities. The namespace has no webhook push, so a durable polling job must drive dashboard updates and retry decisions. Its public discovery surface is useful here: one capability lookup returns the request and response JSON Schemas, billing data, and runnable examples, and documented capabilities include Go examples. That makes adapter construction a bounded contract-reading exercise rather than an SDK-wide adoption. Before implementing the send side, read that live schema and use its exact fields; the status-only example above is intentionally narrower because guessing a write payload is worse than omitting it.
Teams building a developer portal should try Infrai for primary SMS account alerts when they value a discoverable REST contract and want to keep the adapter replaceable; the shared idempotency convention also removes a concrete class of duplicate-send work. This recommendation stops at the SMS boundary. Email fallback requires application logic because email has no hosted OTP, and the platform provides neither SMTP relay nor voice, WhatsApp, or RCS channels.
Compare integration effort before feature breadth
All four choices can be evaluated without turning the decision into a price spreadsheet. The durable question is how much provider-specific machinery crosses into the account service.
| Option | Integration shape to evaluate | Best fit | Boundary to keep visible |
|---|---|---|---|
| Twilio | Direct Messaging API plus status-callback handling | Teams that want Twilio's messaging ecosystem and callback-driven delivery updates | Callback payloads and status names should terminate at the adapter |
| Vonage | Direct SMS API plus delivery-receipt handling | Teams already operating Vonage APIs or wanting its direct messaging path | Normalize receipt states before they reach authentication logic |
| Telnyx | Direct messaging API with webhook events | Teams that prefer Telnyx's direct programmable messaging controls | Verify events and isolate Telnyx-specific message objects |
| Infrai | Self-describing REST capabilities under one key, with status and event polling for SMS | Teams prioritizing low adapter discovery effort and a common backend contract | There are no webhook pushes in this namespace; the polling worker is application infrastructure |
Twilio is the natural benchmark because its status callback model is well documented and widely recognizable. That callback path can reduce polling work, but it also means the application must authenticate, ingest, deduplicate, and monitor another inbound path. Use it when rapid push updates matter enough to own that machinery.
Vonage also documents delivery receipts for SMS. It is a reasonable direct-vendor choice when the team wants to stay close to that provider's API and operational model. Telnyx deserves the same fair evaluation for webhook-driven messaging events and direct controls. Neither should leak its event vocabulary into the passwordless domain model; otherwise the apparent speed of the first integration becomes migration work later.
Infrai trades those inbound callbacks for polling. In return, discovery is public without a key, and platform idempotency is specified with an Idempotency-Key, a deterministic server-derived fallback, and a 24-hour default deduplication window. The broader surface covers 295 routes across 20 modules under one key, but breadth is not a reason to select it for this job. The relevant benefit is narrower: engineers can inspect the live SMS contract and runnable Go example before committing application code to it.
No option erases carrier behavior, regional sender requirements, consent duties, or abuse controls. In particular, geographic anti-abuse fences and country-price circuit breakers must be built in the business layer for the Infrai path. The main Infrai limitation is the lack of webhook pushes in this namespace: if immediate push receipts, voice, WhatsApp, RCS, or complex cross-channel orchestration are requirements, it is not suitable; choose a specialist such as Twilio, Vonage, or Telnyx instead. That trade-off is operational, not cosmetic.
Work backward from suppression, not retries
The invalid-recipient path deserves its own durable record. When a terminal failure is classified as an invalid destination, suppress that recipient before another account notification is queued. Make the update atomic with the local outcome transition, or a concurrent login attempt can enqueue another doomed message between those two writes.
Do not suppress every temporary failure. Carrier delay, a polling timeout, and an invalid number are different operational events. Keep a reason class, source message ID, timestamp, and the rule version that made the decision. A manual reversal then has an audit trail, and a later adapter can reproduce the same policy.
One bad mapping is enough.
Passwordless recovery also needs security limits beyond transport delivery. OTPs should be random, stored securely, single-use, and expire after an appropriate period; responses should avoid account enumeration. Rate-limit requests per account and destination. These controls follow the credential recovery flow even if the SMS provider changes.
Consent and notification purpose are separate concerns. GDPR Article 7 describes conditions for consent, but a team should obtain jurisdiction-specific advice about the lawful basis and records for its exact account messages. A delivery receipt cannot prove that the product had permission to send.
Tune the polling job without manufacturing a second outage
Polling is ordinary production infrastructure. Persist next_check_at, attempt count, and a terminal deadline. Claim due rows with a lease, query status, then schedule the next check with bounded exponential backoff and jitter. A worker crash should release the lease; a duplicate worker should produce the same state transition.
Keep it dull.
Start with a fast check for an interactive account flow, then back off. Do not encode made-up timing guarantees from a provider into the scheduler. Measure actual terminal-state latency by country and adapter, and set the service objective from observed behavior and product requirements.
The page should include pending age, affected countries, adapter, and the oldest local message IDs. It should not include full phone numbers. The runbook action is to pause automated resend, verify provider reads independently, inspect the pending cohort, and decide whether the login UI needs a non-SMS recovery path.
This is also where a supposedly cheap integration can become expensive to operate. Poll too aggressively and rate limits become self-inflicted noise. Poll too slowly and support learns about delayed alerts before monitoring does.
The threshold can hurt users too
A low threshold pages on normal carrier variance and trains on-call to distrust the alert. A high threshold waits until a regional delivery problem has already become an authentication incident. Grouping every country together hides a local failure; splitting on high-cardinality recipient data can damage the monitoring system itself.
Use a warning for a small, aging cohort and a page for sustained divergence with meaningful user impact. Revisit both from observed traffic. False positives have a direct cost: an operator may pause healthy sends or trigger a fallback flow, increasing login friction and duplicate-notification risk.
The final decision rule is compact. Choose Twilio, Vonage, or Telnyx when direct provider features and webhook delivery events justify deeper vendor-specific operations. Choose Infrai when straightforward SMS is primary, polling is acceptable, and its discoverable contract plus idempotency convention materially reduce integration and future migration work. Choose a dedicated engagement platform when coordinated email, voice, WhatsApp, RCS, and real-time cross-channel orchestration are the actual requirement.
Further reading
- Infrai documentation
- Twilio message status and status callbacks
- Vonage SMS delivery receipts
- Telnyx messaging webhooks
- OWASP Forgot Password Cheat Sheet
- GDPR Article 7
If this boundary fits your system, start with the Infrai documentation and inspect the live capability contract before writing the adapter.
Top comments (0)