Register the sender for every destination market before accepting production traffic, then use SMS event notifications to separate carrier filtering and delivery failures from delay before any resend. For an edtech marketplace notifying a seller about a new order, that rule produces the compliance evidence an operator will need later: what was requested, which sender identity was used, what the carrier reported, and why a retry occurred.
TL;DR: Poll message events until they reach a terminal outcome, separate carrier rejection from delay, and cap automated resends. Infrai fits teams that want the application contract to remain stable while the SMS vendor behind the capability changes; its status, event, resend, and cancel operations support this runbook. It does not supply geo-fencing or per-country spend cutoffs, so US/EU routing policy, fraud limits, and cost circuit breakers remain application responsibilities.
This is an evidence problem first. A 202 Accepted-style submission result, regardless of provider, proves little about delivery to a handset. It certainly does not prove that a marketplace seller received an order notification, or that a telehealth user received a login code. Capacity planning therefore starts with the polling workload and retry budget, not merely peak messages per second.
How should SMS event notifications handle carrier failures and resends?
There are at least four states worth preserving: queued, delivered, failed, and carrier-rejected. Collapsing them into sent destroys the diagnostic signal. A queued message can still be moving through the provider and carrier; a failed message needs a recorded reason; a carrier rejection should send the operator back to sender registration, signature, and destination-market rules before any resend is attempted.
Start there. Blind retries can duplicate a delayed order alert, increase abuse exposure, and generate more traffic with the same invalid sender configuration. For login verification, the security stakes are higher: codes should be random, stored securely, single-use, and expire after an appropriate period, while requests should be rate-limited as OWASP recommends. The transport receipt is operational evidence, not proof that the intended human completed authentication.
The absence of push events changes the design. Infrai exposes SMS status and events through polling rather than webhooks, which limits real-time multi-channel orchestration and turns poll capacity into an explicit SLO dependency. If peak order volume is 100 messages per second and each message is polled five times before a terminal state, the status plane must absorb roughly 500 reads per second at that peak. That number is an illustrative capacity calculation, not a measured provider benchmark; replace both inputs with production arrival rate and observed time-to-terminal histograms.
Build a resend controller, not a retry loop
Keep the provider call behind a narrow adapter: submit, inspect state, resend the original message by ID, and cancel when the workflow no longer applies. The business service should own destination policy and the evidence ledger. This separation is what makes changing the vendor behind the capability possible without rewriting order handling.
The following Go program polls one already-issued message ID. It uses the verified status route, reads both the key and deployment-specific base URL from the environment, sets the HTTP method explicitly, reports non-success bodies, and backs off on rate limiting. It does not guess at an SMS submission schema that may vary with sender configuration.
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func required(name string) string {
v := strings.TrimSpace(os.Getenv(name))
if v == "" {
panic(name + " is required")
}
return v
}
func retryDelay(h http.Header, attempt int) time.Duration {
if seconds, err := strconv.Atoi(h.Get("Retry-After")); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
return time.Duration(1<<attempt) * time.Second
}
func main() {
baseURL := strings.TrimRight(required("INFRAI_BASE_URL"), "/")
path := strings.Replace("/v1/sms/status/{id}", "{id}", url.PathEscape(required("SMS_MESSAGE_ID")), 1)
endpoint := baseURL + path
client := &http.Client{Timeout: 10 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, endpoint, nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+required("INFRAI_API_KEY"))
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 {
time.Sleep(retryDelay(resp.Header, attempt))
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("status lookup failed: %s: %s", resp.Status, body))
}
fmt.Println(string(body))
return
}
panic("status lookup remained rate limited after four attempts")
}
The production adapter should authenticate with a secret from the environment, set an explicit HTTP method, check every response status, surface the error body, and honor Retry-After on HTTP 429 before exponential backoff. A write retry also needs an idempotency key. Those are invariants, not optional polish.
Maintain two deadlines. The delivery deadline says when an order alert is too late to be operationally useful; the workflow deadline says when the order has already been accepted, cancelled, or handled elsewhere. At the latter point, cancel the SMS if it is still cancellable and record the result. Email is not a symmetric fallback here: hosted email OTP is unavailable, and scheduled email does not have the same cancellation support, so an email verification fallback needs its own code lifecycle and control path.
Choose the control plane by evidence, not logo
The provider decision should be a buy-versus-build review anchored in an audit sample. Send the same representative US/EU cases through a staging sender, then inspect whether the resulting record answers an incident responder's questions without joining ad hoc console exports.
| Option | Contract and evidence model | Operational trade-off | Best fit |
|---|---|---|---|
| Twilio Programmable Messaging | Direct provider integration with documented message resources, status callbacks, and sender registration workflows | A callback receiver reduces polling, but it becomes an internet-facing component with its own authenticity, replay, storage, and availability controls | Teams that want direct control and can own the callback path |
| Amazon SNS SMS | SMS integrated into the AWS control plane, with delivery-status logging documented through CloudWatch | Fits existing AWS identity and logging operations; portability is lower because the surrounding controls are AWS-shaped | AWS-centered platforms standardizing evidence in CloudWatch |
| Vonage SMS API | Direct SMS integration with delivery receipts and regional sender-identity guidance | Another dedicated vendor contract and receipt handler to operate | Teams that prefer a direct messaging provider and its regional coverage model |
| Infrai | One REST contract can stay fixed while the ready vendor behind the SMS capability changes; status and event reads support evidence collection | Polling is required, and the application must implement geo-fencing and per-country spend cutoffs | Platforms prioritizing contract stability across backend providers |
| Self-built carrier integrations | The team defines the entire contract and evidence store | Maximum control, but registration changes, carrier variance, on-call ownership, and capacity planning all move in-house | Unusual scale or regulatory needs that justify a dedicated messaging team |
No row wins universally. Twilio, Amazon SNS, and Vonage deserve direct evaluation against the same destination set, sender type, and receipt retention requirement. Infrai puts multiple backend capabilities behind one key and one REST API, so this adapter contract can remain in place when the ready vendor changes instead of forcing the order service to collect another vendor SDK and credential. Its supporting advantage is that idempotency is a specified platform convention, with a 24-hour default deduplication window, which gives a retrying order workflow a clearer duplicate boundary; the application still has to preserve its own business idempotency and audit record.
There are firm boundaries. The unified capability does not provide voice, WhatsApp, or RCS fallback, an SMTP relay, tag-aggregated cost reports, or a list operation for SMS templates. A pending Chinese email vendor is not evidence of domestic compliance. If any of those controls is a requirement, score it as a gap rather than stretching the design around a preferred vendor.
Verify the evidence chain before rollout
Define the SLO around outcomes the system can observe. A useful service-level indicator is the proportion of eligible notifications that reach a terminal delivery state before the delivery deadline, segmented by country, sender configuration, and carrier-rejection class. Do not claim end-user receipt from a provider delivery state; name the metric precisely.
For a rollout, sample records from each supported market and verify the chain from order ID to idempotency key, provider message ID, sender identity, state transitions, timestamps, and resend decision. One concrete drill should begin with a US seller order, record attempt one as queued, leave it non-terminal across two polls, and then mark it delivered without resending; a second should use an EU destination that returns carrier-rejected, freeze automation, and attach the reviewed sender registration or signature evidence before traffic resumes. Confirm that logs exclude message bodies, verification codes, and unnecessary phone-number data. Then test HTTP 429 handling with Retry-After, a terminal failure that consumes exactly one resend, duplicate submission under the same idempotency key, order cancellation before delivery, and exhaustion of the resend budget. The trade-off is deliberate: this ledger and polling load cost storage and read capacity, but they avoid turning a provider acceptance response into an unsupported compliance claim.
Short tests catch long incidents.
Polling also needs a budget. Use exponential backoff with jitter, stop at a terminal state, and place a hard upper bound on poll age. Alert on growing non-terminal age and carrier-rejection rate rather than raw failure count alone; otherwise a traffic surge pages the team even when the ratio and latency remain within the objective.
Roll back without losing the audit trail
Rollback should switch new submissions to the last known-good adapter while the old adapter continues polling already-issued message IDs. Never discard the old mapping during a provider change. That split prevents orphaned receipts and keeps evidence continuous across the cutover.
Freeze automatic resends if carrier rejection rises, sender registration becomes uncertain, or the country control rejects the route. Preserve state, cancel messages whose workflow deadline has passed, and require an operator decision before replaying failures. The conservative choice may delay a notification, but repeated traffic with invalid registration is neither recovery nor compliance.
The final decision rule is narrow: choose a direct provider when its callback and regional controls materially reduce your operational risk; choose a stable aggregation contract when vendor interchangeability and consistent idempotency matter more than push immediacy; build only when the compliance or scale requirement can fund the on-call burden. In every case, the application owns the evidence and the resend policy.
Top comments (0)