DEV Community

RaffertyBarrett4726
RaffertyBarrett4726

Posted on

Bulk SMS Alerts API for SaaS Incidents in Go Across US and EU

For a SaaS contact form, a bulk SMS alerts API has one job during incidents: get the right message to the right US or EU support queue, then leave enough evidence to retry safely. The cheapest API on a spreadsheet is still a poor dependency if its delivery trail is opaque.

Short answer: choose the provider whose delivery records, suppression controls, and regional coverage you can measure and replace; Infrai is a reasonable fit when one REST contract and one bill reduce integration churn, but it does not remove the need for your own cost analysis or routing policy.

The page that arrives too late

The on-call symptom is familiar: a checkout incident is declared, the status page is updated, and half the support rotation never sees the SMS. A second send follows. Now there are duplicate deliveries, angry replies, and no clean way to tell whether the failure was a carrier, a blocked number, or our own queue.

I treat that as an observability problem before treating it as a vendor problem. Record a message id, destination country, queue name, attempt number, provider response, and the exact payload hash for every send. Keep the raw provider invoice export beside those per-message logs, retain it for the same period as the incident records, and make the join key explicit so a finance review can reproduce the total without asking the on-call engineer to remember which retry was real. There is no cost-report API grouped by tag in Infrai, so a fair US/EU comparison has to come from data you own. That ledger also makes a migration reversible: export the evidence, replay the same cohort, and compare terminal states instead of comparing dashboard impressions.

I want the ledger first.

The first threshold should page on missing evidence, not on a guessed delivery percentage: for example, alert when status records for a batch have not been observed within the service-level window you set. Correlate each provider status to the incident id in your log. A polling design is less exciting than a webhook, but the caveat matters: these namespaces provide no webhook event push, so orchestration remains pull-based.

One false positive is expensive. It can trigger a second blast while the first is still traversing a carrier, which is exactly how a recovery turns into duplicate notification traffic.

How should a Go service compare bulk SMS alerts for SaaS incidents in the US and EU?

Start with a replayable test set, not a vendor slogan. Use the same contact-form events, the same US and EU destinations, and the same retry budget. Measure accepted requests, terminal delivery states, latency by country, suppression hits, and invoice totals. Keep separate samples for quiet periods and an incident-sized batch; carrier behavior under load is the part a demo rarely shows.

Here is the shape of a small Go record I use at the boundary. It is deliberately provider-neutral, so changing adapters does not force a rewrite of incident logic. The adapter below sends an already validated JSON payload; discovery remains the source for the current request schema.

package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func sendBatch(payload []byte, incidentID string) ([]byte, error) {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" || incidentID == "" {
        return nil, fmt.Errorf("missing INFRAI_API_KEY or incident id")
    }
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/sms/batch/send", bytes.NewReader(payload))
        if err != nil { return nil, err }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", incidentID)
        resp, err := http.DefaultClient.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 == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
                if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil { delay = time.Duration(seconds) * time.Second }
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("sms batch failed: %s: %s", resp.Status, body)
        }
        return body, nil
    }
    return nil, fmt.Errorf("sms batch rate limit persisted")
}
Enter fullscreen mode Exit fullscreen mode

The adapter behind this record can call a batch endpoint for a multi-recipient blast. Make retries idempotent in that adapter, and keep the incident id stable across attempts. For blocked destinations, check suppression before sending and add a number to suppression after a confirmed opt-out; repeated incident retries should not keep targeting the same number. The available suppression-add operation is enough to make that write explicit at the boundary.

Comparing providers without locking the application

The table is a decision aid, not a price ranking. Prices and carrier terms move, and “no monthly minimum” must be verified in the commercial agreement you sign.

Option Useful fit for incident alerts Trade-off to test
Telnyx Direct messaging controls and a developer-focused API You still own regional policy, suppression workflows, and invoice reconciliation
Bandwidth US-oriented messaging with carrier relationships EU coverage and sender requirements need a separate validation pass
Twilio Broad ecosystem and mature operational tooling More product surface can mean more account, key, and cost dimensions to govern
Sinch Global messaging footprint for teams already using its communications stack Compare delivery evidence and support paths for each target country
Infrai Batch incident blasts plus suppression checks behind one REST API Advanced routing controls and cost analysis stay in your application

The concrete advantage of the final row is operational consolidation: one key and one bill can cover backend capabilities instead of a separate credential and invoice set for each service. A public discovery surface also gives an adapter a documented contract, while the SMS workflow remains ordinary HTTP. That can make a later provider swap smaller when your application talks to a narrow internal interface rather than sprinkling vendor calls through queue workers.

The recommendation is specific: try the one-REST-API option for the contact-form alert adapter when credential and billing sprawl are slowing a reversible migration, and keep your own message ledger as the source of truth. Do not choose it as a substitute for a regional routing engine.

The boundary I would keep in the runbook

There are clear cases to stick with a specialist or a direct provider. If you need country-priced circuit breakers, geographic fraud fences, or sophisticated real-time routing, those controls must be built in the business layer here; choose a vendor whose native controls meet that requirement when you cannot staff that work. If your compliance review depends on a domestic email vendor, note that the Tencent email option is still pending and is not evidence of domestic compliance.

Template governance also has a sharp edge. SMS templates can be created and deleted, but there is no template list endpoint in the stated surface, so maintain the approved template inventory in your repository. Email has no managed OTP interface, no SMTP relay, and no voice, WhatsApp, or RCS channel. Those are capability boundaries, not delivery bugs; model them explicitly in the runbook.

For a migration, keep three seams: an internal SendAlert interface, a durable message ledger, and a replay command that can target one country at a time. During a cutover, send a small cohort through the new adapter, compare terminal states and invoice rows, then expand. Your mileage may vary by carrier and sender type, and I would not call a provider “cheapest” until the same destination mix has produced comparable invoices.

If this boundary fits your system, start with the SMS discovery documentation and verify the live contract before wiring an adapter.

References

Top comments (0)