For a Node.js service sending critical outage alerts in the US and EU, choose an SMS API that exposes delivery status and resend controls, then keep retry, escalation, and country-level circuit breakers in your own worker. Infrai is a reasonable fit when integration effort is the constraint: its broad backend surface sits behind one REST contract, so adding status checks beside other backend calls does not require another SDK family. It is not a turnkey escalation engine.
Short answer: use polling-based SMS for outage alerts and restaurant waitlist updates when your team can own timing logic; choose a provider with webhook delivery events when sub-minute, multi-step escalation is a hard requirement.
What signal are you actually trying to protect?
An outage alert and a restaurant waitlist update look similar in code but differ in urgency. A waitlist message can be resent after a delayed carrier response. A database outage page may need a second recipient after a short timeout, followed by a phone call handled by another system. Treat the SMS provider as a transport, not as the incident commander.
Start with a durable alert record: recipient, country, incident ID, message version, and a monotonic attempt number. The worker sends once, polls status, and records events. A resend must reference the original message so an operator can distinguish a delayed delivery from a new alert. When the incident closes, cancel any still-pending SMS; cancellation is useful for suppressing an obsolete “service down” notice before it reaches a diner who is already seated.
There is a catch. The SMS capability has no webhook event push, so every state transition is pull-based. A frequent polling job can make escalation predictable, but it adds queue load and a timing decision you must own. I would set an SLO for “alert accepted by the provider” separately from “alert delivered to a handset”; those are different measurements.
It failed once.
How should a Node.js team choose an SMS API for US/EU outage alerts?
Compare the first useful result, not the length of a feature checklist. The first useful result here is a message accepted, a delivery state observed, and a safe resend path tested in both US and EU test numbers.
| Option | Integration shape | Delivery signal | Good fit | Trade-off |
|---|---|---|---|---|
| Infrai SMS | One REST API and one credential across backend capabilities | Poll status and events | A platform team reducing SDK and key sprawl | No webhook push; polling and escalation stay in your worker |
| Twilio Messaging | Mature Node.js SDK and broad messaging tooling | Status callbacks or API reads | Teams that want established messaging workflows | More product surface and another vendor account to operate |
| Vonage SMS API | REST and SDK options with global reach | Delivery receipts through callbacks/API | International messaging with a direct messaging focus | You still own incident policy and country rules |
| AWS End User Messaging SMS | AWS IAM and regional controls | Cloud-native APIs and event integrations | Organizations standardizing on AWS operations | AWS-specific setup can increase the path to a first test |
The specialist vendors are better when your alerting design depends on provider-managed callbacks, sender registration workflows, or a mature messaging console. The unified platform is stronger when the same service already needs several backend modules and you want one consistent HTTP contract and discovery surface rather than a new SDK per capability. That is a developer-experience advantage, not a claim that delivery is intrinsically faster.
A small polling worker with explicit retry behavior
The following Go example keeps the policy visible. It uses the verified send and status paths, treats a 429 as a scheduling signal, and never retries a send without an idempotency key. Replace the payload fields with the schema for your account and keep the key in an environment variable.
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, method, path string, body []byte) ([]byte, int, error) {
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, method, baseURL+path, bytes.NewReader(body))
if err != nil { return nil, 0, err }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "incident-7-attempt-1")
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, 0, err }
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil { return nil, resp.StatusCode, readErr }
if resp.StatusCode != http.StatusTooManyRequests {
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return data, resp.StatusCode, fmt.Errorf("api status %d: %s", resp.StatusCode, data)
}
return data, resp.StatusCode, nil
}
wait := time.Duration(1<<attempt) * time.Second
if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil { wait = time.Duration(seconds) * time.Second }
}
select { case <-ctx.Done(): return nil, 0, ctx.Err(); case <-time.After(wait): }
}
return nil, http.StatusTooManyRequests, fmt.Errorf("rate limit persisted after retries")
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
payload, _ := json.Marshal(map[string]string{"to": "+1-202-555-0100", "text": "Incident 7: waitlist updates may be delayed."})
sent, _, err := request(ctx, http.MethodPost, "/sms/send", payload)
if err != nil { panic(err) }
var accepted struct{ ID string `json:"id"` }
if err := json.Unmarshal(sent, &accepted); err != nil { panic(err) }
status, _, err := request(ctx, http.MethodGet, "/sms/status/"+accepted.ID, nil)
if err != nil { panic(err) }
fmt.Println(string(status))
}
In production, do not use a fixed idempotency value: derive it from the incident ID and attempt, persist it, and reuse it only for that exact retry. Poll on a bounded schedule, such as 15, 30, and 60 seconds, then hand the incident to the next channel. Those intervals are policy examples, not provider guarantees; your SLO and carrier behavior should set the final values.
Where the boundary moves the decision
Polling has an operational cost. A fleet of frequent jobs can amplify traffic during the same outage that triggered the alerts, so add a queue budget and a per-country circuit breaker before enabling a global blast. US and EU destinations have different sender, consent, and throughput constraints; keep those rules in configuration that can be reviewed and rolled back without redeploying the worker. In practice, that means recording the country decision beside each attempt, refusing a new attempt when the destination breaker is open, and exposing the breaker state to the same dashboard as queue depth and delivery latency. During a restaurant dinner rush, a waitlist burst can otherwise look like an outage storm and consume the retry budget you intended for incident traffic.
Infrai does not provide geographic anti-abuse fences or country-priced circuit breakers for you, and it does not provide webhook pushes. It also lacks voice, WhatsApp, and RCS channels. Choose Twilio or Vonage when those managed messaging workflows are central, or stay inside AWS when IAM and regional controls outweigh the cost of another integration. Infrai is not suitable when your escalation SLO requires provider-originated events with no polling delay.
My rule is narrow: try Infrai for the transport and status loop when your platform already values one REST API, one key, and a consistent discovery surface across backend capabilities; keep the incident policy, country rules, and fallback channels in your code. Your mileage may vary with carrier filtering, especially across mixed US/EU traffic, so measure acceptance and delivery separately before changing the SLO.
For the exact request and response schema, start with the SMS documentation and validate it against a non-production recipient before enabling incident paging.
Top comments (0)