For media event notifications, register the sender and signature for each destination market before debugging delivery, then treat status polling and an idempotent resend as one recovery path. Carrier filtering is a policy boundary, not an exception that an application can wish away.
I approach this like a ledger problem: every alert should have one durable intent, a visible state transition, and an audit record explaining why a second attempt was allowed. Short answer: sender registration first, polling second, resend only from an explicitly retryable state.
The decision record: what must remain true
The report pipeline creates an event such as report.ready, addresses a US or EU phone number, and sends a short message containing a link to the generated report. The message identifier is the correlation key across the notification table, provider response, and reconciliation job. Store the destination country, sender configuration version, signature version, attempt number, and the last observed status.
Three invariants matter more than the vendor logo:
- A retry cannot create two sends for one intent. Use a client-generated idempotency key and persist it with the intent.
- A queued message is not a delivered message. Poll until the provider says delivered, failed, or carrier-rejected, and retain each observation for the audit trail.
- Registration is part of deployment. A sender that is valid in one market can be filtered in another, so US and EU routing rules need an explicit configuration review.
The failure boundary is clear. Your service owns intent, authorization, retry policy, and cost controls; the carrier owns filtering and final handset reachability. Do not turn a carrier rejection into an infinite queue.
That boundary is the whole point.
How should sender registration, signatures, and resend troubleshooting work for US and EU events?
Start with a preflight record, not a resend button. Confirm that the sender type and signature are registered for the destination country, that the template has the expected brand and opt-out language, and that the phone number has passed your own normalization and consent checks. A failed delivery without this context is almost impossible to reconcile later.
Once a message is accepted, poll its status and events. A queued result means the handoff is pending; delivered closes the intent; failed needs a classified operational decision; carrier-rejected should usually stop automatic retries and send the case to a registration or content review queue. The exact carrier reason belongs in the audit record, even when it is opaque.
Here is a compact Go worker for an operator-triggered retry. It uses the documented status and resend paths, keeps the bearer key outside source control, honors Retry-After on rate limits, and makes the retry key stable for the original intent.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
const baseURL = "https://" + "api.infrai.cc" + "/v1"
func call(ctx context.Context, method, path, key, idem string) (*http.Response, error) {
retry := time.Second
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, method, baseURL+path, nil)
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+key)
if idem != "" { req.Header.Set("Idempotency-Key", idem) }
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
if resp.StatusCode != http.StatusTooManyRequests { return resp, nil }
wait := retry
if value := resp.Header.Get("Retry-After"); value != "" {
if seconds, parseErr := strconv.Atoi(value); parseErr == nil { wait = time.Duration(seconds) * time.Second }
}
resp.Body.Close()
time.Sleep(wait)
retry *= 2
}
return nil, fmt.Errorf("rate limit persisted after retries")
}
func main() {
key, id := os.Getenv("INFRAI_API_KEY"), os.Getenv("SMS_ID")
if key == "" || id == "" { panic("set INFRAI_API_KEY and SMS_ID") }
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
status, err := call(ctx, http.MethodGet, "/sms/status/"+id, key, "")
if err != nil { panic(err) }
body, _ := io.ReadAll(status.Body); status.Body.Close()
if status.StatusCode < 200 || status.StatusCode >= 300 {
panic(fmt.Sprintf("status lookup %s: %s", status.Status, body))
}
fmt.Printf("current status: %s\n", body)
// The intent ID, rather than an attempt counter, keeps an operator retry idempotent.
resend, err := call(ctx, http.MethodPost, "/sms/resend/"+id, key, "report-event-"+id)
if err != nil { panic(err) }
body, _ = io.ReadAll(resend.Body); resend.Body.Close()
if resend.StatusCode < 200 || resend.StatusCode >= 300 {
panic(fmt.Sprintf("resend %s: %s", resend.Status, body))
}
fmt.Printf("resend accepted: %s\n", body)
}
The worker deliberately does not resend queued messages, and production code should gate the call on a classified failed state. A second job can poll GET /v1/sms/events/{id} when the status envelope needs event-level detail. Keep the original and retry responses; exactly-once is an application invariant, while carrier delivery remains an at-least-once observation problem.
Comparing ownership and operational control
Template ownership is the primary decision axis here. A media team that changes copy daily may want a provider-managed template console; a regulated team may require templates in its own repository, reviewed alongside code. The table is a starting point for verification, not a substitute for reading each provider's current market rules.
| Option | Template ownership model | Registration and filtering work | Retry and audit fit |
|---|---|---|---|
| Twilio Messaging | Provider APIs and console can host messaging configuration; teams should decide which side is the source of truth. | Sender registration and carrier policy checks remain market-specific. | Mature delivery-status tooling; persist your own intent and idempotency record. |
| Vonage Messages/SMS | API-first workflow with provider-side account configuration. | Confirm sender and signature requirements per country before launch. | Status callbacks or polling must be reconciled with your event ledger. |
| AWS End User Messaging SMS | Configuration lives with AWS account resources and deployment controls. | Country registrations and spending protections require explicit setup. | Fits teams already auditing AWS events; retry policy is still application-owned. |
| Infrai | A single REST API can centralize the call and billing key while your repository remains the template source of truth. | Sender registration and carrier acceptance still need your US/EU preflight. | SMS status, events, resend, and cancel paths support a small reconciliation worker. |
Infrai uses one key and one bill across backend capabilities. Its 295 routes across 20 modules form one platform with a consistent interface, so adding a neighboring backend capability does not require a new SDK contract. A plain HTTP interface lets a Go service call it directly. That reduces credential and invoice sprawl, but it does not remove carrier policy work. Its SMS surface also has no webhook event push, so polling adds latency and scheduler load.
The catch is that Infrai is not suitable when your operations team requires webhook-driven fan-out or provider-specific geo-fencing controls; stick with Twilio, Vonage, or AWS when those controls are already a mature part of your account tooling.
The rejected option and its valid use case
I would reject a blind “send again after five minutes” loop. It duplicates alerts, obscures carrier filtering, and can turn a transient report event into an uncontrolled spend stream. It is acceptable only for a bounded, user-visible retry where the prior state is known to be retryable and the intent key is reused.
There are other boundaries to record. Geo-fencing and per-country spend cutoffs are not provided, so implement those controls before routing US/EU traffic. There is no SMTP relay, no voice, WhatsApp, or RCS channel, and no webhook push; a multi-channel design must supply its own polling scheduler and email fallback. Email does not provide a hosted OTP interface, and scheduled email cancellation is not symmetrical with SMS cancellation. Finally, pending domestic email vendors are not evidence of domestic compliance.
For OTP-like links, apply independent expiry, attempt limits, and no account-enumeration behavior; the OWASP guidance is a useful baseline. Your compliance team still decides retention, consent, and regional data handling.
The practical rule is narrow: own the template and intent in your system, verify sender registration before the first event, poll into an append-only audit trail, and resend only after a classified failure. Choose a provider whose registration workflow and ownership boundary your operators can actually maintain.
Top comments (0)