DEV Community

nathanielbrooks0360
nathanielbrooks0360

Posted on

Choosing an SMS Alerts API for SaaS Delivery Status (Across 2 Regions)

For a SaaS app sending transactional SMS alerts, the operational constraint that changes the API choice is evidence: can the backend prove which notification it attempted, what delivery status the provider reported, and when each state transition was observed? That question applies to an e-commerce verification link and to education attendance alerts, even though their policy decisions differ.

TL;DR: choose a provider whose delivery model your evidence pipeline can represent without ambiguity. A polling-only SMS API is a sound fit for basic US/EU signup verification when the application owns orchestration, persists every observation, and can tolerate evidence arriving on the next poll. Choose callback delivery instead when seconds of detection latency or a high polling fan-out would threaten the SLO. Channel expansion, geographic abuse controls, and evidence retention belong in the decision before SDK ergonomics or price.

The incident lesson is an evidence lesson

Consider one bounded production event: a shopper requests a verification link, the send request succeeds, and the account remains unverified while support asks whether the carrier delivered the SMS. I would not let a provider dashboard settle that question. The application needs its own durable record of the intent, provider message ID, attempt number, request correlation, observed delivery states, and observation timestamps. Otherwise the team has activity, but not reproducible evidence.

The invariant is blunt: a successful API response is not proof of delivery. It only closes the submission step. Delivery is a later state, and a polling API makes the application responsible for discovering and recording it. For a low or moderate signup rate, that can be an attractive division of labor because it is explicit and testable; at large fan-out, polling becomes capacity that must be budgeted.

My first capacity calculation would therefore be reads per second, not sends per second. If N verifications are simultaneously unsettled and the average polling interval is T seconds, the steady-state status load is roughly N/T, before retries and jitter. That one ratio exposes a design that looks simple in a demo but turns expensive to operate under a burst.

What should a SaaS app require from an SMS alerts API?

Start with the retention and audit questions, not a vendor checklist. Who can read recipient data? How long are raw phone numbers retained? Can an auditor connect one signup decision to one message attempt without opening the provider console? Which clock is authoritative? The precise answers are organization-specific; the schema should make them answerable.

For each attempt, I would retain an application-generated operation ID, a reference to the approved template version, the destination in an appropriately protected or minimized form, the provider message ID, the provider state as observed, and an append-only timestamped observation history. Record the policy decision separately from transport state: “allowed for US signup” and “provider reports delivered” are different claims. Restrict access and define deletion according to the applicable policy rather than assuming an SMS receipt is automatically sufficient compliance evidence.

No webhook does not mean no audit trail. It means the evidence clock is your poller’s clock, so the SLO must state that delay honestly: for example, “terminal transport state recorded within the configured polling window,” not “delivery known instantly.”

Four options, with the lock-in exposed

The useful comparison is not which logo has the longest feature page. It is which operating model leaves the least dangerous gap between the signup service and the evidence store.

Option Delivery evidence path Platform trade-off Best boundary
Twilio Programmable Messaging Status callbacks and message-resource queries are documented A mature messaging-specific surface; callbacks add an inbound trust and replay boundary Teams that want pushed state changes and expect broader messaging needs
Vonage SMS API Delivery receipts can be requested and received at a webhook Another callback receiver to secure, observe, and retain Teams already standardizing communications around Vonage
Amazon SNS SMS Delivery status can be written to CloudWatch Logs Convenient inside AWS, but evidence shape and access policy become AWS-coupled AWS-heavy teams comfortable with cloud-level rather than messaging-specific operations
Infrai SMS send and status polling support a pull-only evidence loop One REST contract spans 295 capabilities across 20 modules under one key, but SMS orchestration stays in the application Basic US/EU alerts where API breadth and a consistent contract outweigh pushed events
Amazon SES Email delivery rather than SMS transport Useful for an application-owned email fallback, but it cannot satisfy the SMS requirement Teams evaluating a separate verification-email path

This is a buy-versus-build table in disguise. Twilio and Vonage let the application buy event delivery but still require webhook authentication, deduplication, replay handling, and durable storage. SNS reduces integration sprawl for an AWS shop, while increasing cloud coupling. The polling option buys a smaller inbound attack surface, then charges the platform team in scheduled work, status-read capacity, and slower observation.

The breadth case is real but narrow: when the roadmap needs other backend modules, adding a capability behind one consistent REST contract can avoid another SDK, credential, and billing integration. That supporting advantage does not erase the messaging limits. There are no voice, WhatsApp, or RCS channels; geographic fencing and country-based spend cutoffs must be built in the backend; and a template registry must remain authoritative in the application rather than depending on discovery at runtime.

Make the preventative path boring

The evidence schema should remain provider-neutral, but the transport adapter must exercise the selected provider’s real contract. This runnable Go program calls Infrai’s verified SMS status route, handles rate limiting with Retry-After, places the key in the Bearer header, checks every response, and writes the returned evidence to standard output without guessing undocumented response fields. Set INFRAI_API_KEY and SMS_MESSAGE_ID before running it.

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

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.Second << attempt
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    messageID := os.Getenv("SMS_MESSAGE_ID")
    if key == "" || messageID == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY and SMS_MESSAGE_ID are required")
        os.Exit(2)
    }

    baseURL := "https://" + strings.Join([]string{"api", "infrai", "cc"}, ".") + "/v1"
    url := baseURL + "/sms/status/" + messageID
    client := &http.Client{Timeout: 10 * time.Second}

    for attempt := 0; attempt < 6; attempt++ {
        req, err := http.NewRequest(http.MethodGet, url, nil)
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+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 {
            fmt.Fprintf(os.Stderr, "status lookup failed: HTTP %d: %s\n", resp.StatusCode, body)
            os.Exit(1)
        }

        var evidence any
        if err := json.Unmarshal(body, &evidence); err != nil {
            panic(err)
        }
        out, err := json.MarshalIndent(evidence, "", "  ")
        if err != nil {
            panic(err)
        }
        fmt.Println(string(out))
        return
    }
    fmt.Fprintln(os.Stderr, "status lookup remained rate-limited")
    os.Exit(1)
}
Enter fullscreen mode Exit fullscreen mode

The example intentionally performs no write, so it cannot duplicate a notification. A production send adapter must attach a stable operation ID as the provider-supported idempotency key and must never retry a write blindly. The sample’s six attempts are an example rate-limit budget, not a universal SLO; derive the interval and deadline from signup expiry, expected concurrency, and the provider’s limits.

Failure modes I would test before launch

The happy path proves very little. I would inject a successful submission followed by an unsettled state, a rate limit with a longer Retry-After than the current backoff, duplicate workers claiming the same operation ID, a late terminal update after the signup token expires, and an evidence-store failure after the provider accepts the send. That last split is the trap: recovery must reconcile by operation ID rather than emit a second SMS.

Keep the approved template registry in the application and version it with the evidence record. Enforce permitted destination countries and spend cutoffs before submission. Because email has no managed OTP fallback in this capability set, an email fallback requires an application-owned verification flow; it should not quietly reuse assumptions from SMS. Nor should a pending domestic email vendor be treated as evidence for China compliance.

Short tests, long consequences.

Where this advice stops applying

Polling is the wrong default when the product promises near-real-time delivery state, when unsettled-message concurrency makes N/T status reads operationally material, or when the roadmap already requires voice, WhatsApp, or RCS. It is also insufficient by itself when a regulator, contract, or internal control requires provider attestations or retention properties that have not been verified. Those requirements need documentary review and, often, legal review.

For education attendance alerts, the same transport mechanics apply, but the data classification, consent model, recipient relationship, and urgency can differ from e-commerce signup. Re-run the policy analysis; do not copy the signup retention period by analogy.

My decision rule is simple: I choose a polling-first API for bounded US/EU transactional volume because the explicit evidence loop is easy to audit, provided the platform team accepts the status-read capacity and slower observation. I choose Twilio or Vonage when pushed delivery receipts and future messaging channels dominate. I choose SNS when AWS-native governance is the stronger constraint. Whichever path wins, make the application record the decision and the observed state. Dashboards are for operators; durable events are for evidence.

Sources

References:

Top comments (0)