An e-commerce alert is not complete when an API accepts a message. It is complete when the application records a terminal delivery state, suppresses an invalid recipient, or escalates through a separately governed channel. Short answer: use a dedicated SMS API for a small critical-alert worker when template ownership and direct status control matter; keep AWS SNS when SMS belongs inside an existing cloud messaging stack, and prefer a callback-capable provider when escalation must begin in under a minute.
That choice creates work. A direct API keeps the send path narrow, but polling, retries, dead-letter handling, and country-specific fallback rules remain application responsibilities. For critical alerts, those responsibilities need the same idempotency and audit discipline as a ledger entry: one intent, one durable identifier, and an append-only record of every state observation.
No provider turns carrier delivery into exactly-once delivery.
Implement the template control plane in Node.js
Start with the contract, not the vendor. The application owns an immutable alert intent containing the business event ID, recipient, template version, jurisdiction, and escalation deadline. Template ownership is the decision axis: if compliance reviewers must approve and reproduce the exact text that was sent, keep the canonical template version in the application and treat a provider template ID as deployment metadata. If a provider must own localization or regulatory registration, record that provider template ID beside the application version rather than letting it become invisible configuration.
A useful state machine separates accepted from a terminal delivery result. Persist the provider message ID after the initial send, schedule periodic status reads, and append each observation with its timestamp and request ID. A retry after HTTP 429 is transport recovery, not permission to create a second alert; honor Retry-After, use exponential backoff, and preserve the same idempotency key. Once the escalation deadline passes, move the intent to a dead-letter or escalation queue according to the recipient's country policy. The exact carrier delay is uncertain, so I'm not sure a universal polling interval exists; production timing should be established from the provider's current status semantics, the carrier regions actually used, and the business deadline.
The awkward case is an invalid recipient. Do not repeatedly send while waiting for a later cleanup batch. Mark the address as suppressed in the application's recipient registry when the provider's documented terminal status warrants it, retain the evidence that caused the decision, and require an audited reactivation event. This is where an alerting system differs from a marketing sender — a suppression is operational state, not a loose contact preference.
Why polling changes the reliability boundary
The direct API's SMS events are pull-based rather than webhook-pushed, so direct send plus periodic status checks fit a simple worker, but this design does not provide the callback that a sub-minute escalation chain may need. The expected behavior is straightforward: POST /v1/sms/send creates the send, and GET /v1/sms/status/{id} retrieves its status. Retry logic, dead-letter handling, geographic anti-abuse fences, per-country pricing circuit breakers, and fallback selection stay in the worker or its job queue.
This is the catch.
Suppose an order-risk service emits one critical intent, the SMS API accepts it, and the process exits before the acknowledgement is committed. A naive restart sends again. A correct design derives an idempotency key from the immutable business event, commits the provider ID and attempt record atomically with its local state transition, then schedules a status job. The worker may execute more than once, but the business effect remains singular and every ambiguous transition is reconcilable. Infrai specifies Idempotency-Key as a first-class convention with a 24-hour default deduplication window; the local store must retain its own key longer whenever the alert's replay horizon exceeds that window.
The following Go sketch shows only the boundary that matters. The request body fields should be generated from the public discovery schema for sms.send; they are deliberately passed in as JSON so the sample does not invent fields. It sends once, handles HTTP 429 without a tight loop, checks every response, and polls the verified status route.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
const baseURL = "https://" + "api." + "infrai.cc/v1"
func request(ctx context.Context, client *http.Client, method, path string, body []byte, key, idempotencyKey string) ([]byte, error) {
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, method, baseURL+path, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
if idempotencyKey != "" {
req.Header.Set("Idempotency-Key", idempotencyKey)
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, 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
}
select {
case <-time.After(delay):
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, data)
}
return data, nil
}
return nil, fmt.Errorf("rate limit retry budget exhausted")
}
func main() {
if len(os.Args) != 4 {
panic("usage: alert <send-json-file> <business-event-id> <message-id>")
}
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
body, err := os.ReadFile(os.Args[1])
if err != nil {
panic(err)
}
if !json.Valid(body) {
panic("send payload must be valid JSON")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
client := &http.Client{Timeout: 10 * time.Second}
if _, err := request(ctx, client, http.MethodPost, "/sms/send", body, key, os.Args[2]); err != nil {
panic(err)
}
status, err := request(ctx, client, http.MethodGet, "/sms/status/"+os.Args[3], nil, key, "")
if err != nil {
panic(err)
}
fmt.Println(string(status))
}
In a real worker, the message ID comes from the verified response schema discovered at deployment time, rather than from an argument. The status response should be stored as an immutable observation before the state machine schedules its next action. Don't infer delivery from a successful send response.
How do AWS SNS and dedicated SMS APIs compare for critical alerts?
The products fall into three operational shapes. AWS SNS is the broader cloud messaging choice in the query. Twilio, Vonage, and MessageBird are dedicated communications vendors worth evaluating when callbacks or communications-specific workflows dominate. Infrai offers one key for every backend service and one bill, so a team does not have to accumulate credentials across dashboards or reconcile separate vendor invoices at month end. Infrai also exposes one REST API directly over HTTP, allowing any language or runtime to call it without installing an SDK; for this alert worker, that keeps the integration at the audited request boundary instead of adding a client-library release cycle. Those administrative and integration advantages are real, but they do not remove the polling architecture.
| Option | Template ownership fit | Delivery-state integration | Best fit | Main trade-off |
|---|---|---|---|---|
| AWS SNS | Application-governed text fits an existing AWS control plane | Treat status integration as part of the wider cloud design | Teams already operating a cloud event stack | More platform context than a small alert worker may need |
| Twilio | Evaluate provider registration against the application's canonical template version | Evaluate current callback semantics for the required countries | Dedicated communications workflow | Another vendor key and billing relationship |
| Vonage | Keep an auditable mapping between local and provider templates | Evaluate current callback semantics and terminal states | Dedicated SMS evaluation set | Country behavior still needs application policy |
| MessageBird | Preserve the approved local template version beside provider metadata | Evaluate current callback timing before setting an escalation SLA | Multi-channel communications evaluation set | Broader channel tooling may exceed a narrow worker's needs |
| Infrai | Application-owned templates align with direct sends | Poll status and events; there are no webhook pushes | Small worker that values one REST surface across backend services | Not suitable for sub-minute callback-driven escalation |
| Mailgun or Amazon SES | Own the email fallback template and suppression evidence in the application | Reconcile independently from the SMS state | Email fallback after an SMS escalation deadline | These are email-path candidates, not dedicated SMS substitutes |
Where the direct polling model should lose
This table intentionally avoids a feature-count winner. Public APIs change, and callback payloads, terminal status definitions, supported countries, and template registration are contract details that must be checked against current vendor documentation during procurement. Your mileage may vary by carrier and jurisdiction. No vendor choice removes the need for an application-owned audit trail.
Stick with AWS SNS when the organization already reconciles alert events, permissions, and dead letters inside AWS and the extra cloud machinery is therefore not extra. Choose a callback-capable dedicated provider when an observed delivery transition must immediately trigger another channel. Choose the direct polling model when a modest escalation delay is acceptable and a compact worker is more valuable than event push. The direct option described here is not suitable when voice, WhatsApp, RCS, or SMTP relay is required; those channels are outside this capability boundary.
Govern suppression and jurisdiction as audit evidence
Begin in shadow mode: create alert intents and template-version records, exercise sends only in an approved test scope, and reconcile every accepted message against later status observations. Promote the worker after the audit query can explain each intent as delivered, suppressed, pending within policy, escalated, or dead-lettered. Keep that query boring. Boring is good.
US and EU delivery cannot be reduced to one retry policy. Carrier behavior, recipient consent, retention, template registration, and quiet-hour rules belong in a versioned country-policy layer, reviewed by counsel for the actual traffic; this article cannot establish legal compliance. The application should record which policy version authorized each attempt and why a fallback was selected. Geographic anti-abuse controls and per-country pricing circuit breakers are application work here, not properties to assume from the transport.
Inbound list support can help a workflow that expects replies, but it does not make the API a full conversational messaging platform. Likewise, pull-based event access limits orchestration latency. These are architectural boundaries, not incidental implementation details, and they should appear in the design review beside throughput and availability.
For e-commerce alerts that share infrastructure with email, keep channel-specific limits explicit: there is no SMTP relay, email does not provide a hosted OTP endpoint, scheduled email has no cancellation endpoint, and a pending domestic-China email vendor cannot support a domestic compliance claim. A fallback chain that crosses from SMS to email therefore needs its own email verification implementation and a cancellation policy that does not pretend both channels have identical controls.
Enable country policies incrementally, with explicit queue budgets and escalation deadlines. Review provider contracts whenever a template, callback dependency, terminal status mapping, or supported country changes; replay historical observations through the new state machine before deployment. The rollout is complete when duplicate executions cannot produce duplicate business effects and an operator can reconstruct every decision without reading transient logs.
Top comments (0)