DEV Community

QuentinBarrett5281
QuentinBarrett5281

Posted on

SMS Event Notification Alerts: App vs Provider Templates, 2 Delivery Status Rules

Choose application-owned templates for marketplace SMS event notification alerts when customer support must explain delivery status for both the original send and a recovery attempt. The deciding constraint is who can reconstruct the exact message a seller received after the template or delivery provider changes.

TL;DR: use SMS as a secondary or urgent channel, retain the template revision and order metadata in your application, and poll delivery status before offering a resend. Keep cancellation for pending scheduled messages. Enforce seller cooldowns, destination-country allowlists, and spend thresholds before dispatch, including dispatch from the support console.

A capability contract can keep the application call stable while the provider behind it changes. That helps this workflow when the application also retains the meaning of the message: swapping a transport should not silently change what “new order” tells a seller to do.

What page fired when the seller missed the order?

Start the investigation with that question.

A dashboard showing successful submissions is weak evidence for the support question, which is whether a particular seller received a particular order notification in time to act. Keep the order event, notification intent, provider message identifier, template revision, and last observed delivery state associated in your own records. These are proposed application records, not fields to assume a messaging API accepts.

Submission isn't delivery.

For an illustrative support ticket, use order ORD-1842, seller SELLER-27, and template seller-new-order revision 7. The deterministic copy might be “New order ORD-1842. Open your seller account for details.” Avoid generating fresh prose during a retry: support needs to distinguish another attempt to deliver the same notification from a new message with different instructions. Store the rendered text, or a reproducible rendering record with appropriate access controls, alongside the revision.

The postmortem should separate a missing order event, a rejected dispatch, an unresolved delivery attempt, and a delivered message that the seller did not act on. Those lead to different repairs. Resending every unresolved attempt makes a reassuring graph while leaving the causal question unanswered, and it can give a seller duplicate instructions for a single order.

Choose the template owner before the transport

Application ownership means support and engineering can identify the approved copy and its revision independently of the sending provider. Provider ownership can be reasonable when the team wants the provider's template workflow to be authoritative, but then a provider change also becomes a content migration that needs review. Neither choice removes the need to store the order-to-notification relationship.

For this marketplace workflow, choose application ownership because support needs reproducible answers across the original send and a later recovery action. The trade-off is real: the application team owns rendering, revision retention, approval rules, and the mapping to any provider template identifiers. If that team cannot operate those controls, keeping the provider as the template authority may be the more honest decision.

Candidate Relevant integration boundary What to establish before choosing it
Twilio Programmable Messaging A messaging-specific integration Confirm how your application records message identifiers and delivery evidence while keeping the approved order copy reproducible.
Amazon SNS Topic-based notification fanout as well as SMS publishing Decide whether the order event needs a fanout boundary; retain seller-specific recovery decisions outside that boundary.
Vonage SMS API A direct SMS API integration Confirm the mapping between your notification intent and SMS delivery records, including recovery behavior.

These are different integration shapes, not an uptime ranking. Twilio and Vonage are natural candidates for a team organizing its adapter around individual SMS messages; SNS deserves consideration when publishing an order event to multiple subscribers is part of the design. Verify each candidate's current request and delivery mechanisms in its primary documentation. Do not assume identical resend, cancellation, template, or country-control semantics across them.

Infrai is another fit when keeping one REST API contract stable across underlying vendors matters, with one key and one bill covering SMS and email so the order-support worker can use a single credential for both channels. Changing the provider behind a capability need not change the application call. Its public discovery surface describes request and response schemas, and per-call cost, vendor, latency, and request metadata can support the order investigation. Its communication events use polling, so build the support view around the last observation; keep country controls and spend thresholds in the application, and maintain business metadata there because per-call metadata does not provide a tag-aggregated cost reporting API. Application-level authorization must still decide which action a support operator may take.

Pick the integration whose delivery evidence and template ownership model your support team can operate. A stable contract is useful when provider substitution is a requirement, while a direct integration is reasonable when provider-specific behavior is worth the coupling.

Put the guard before every dispatch

A support resend button is another sending surface. Route it through the same suppression, country, cooldown, and budget checks as the initial order alert; otherwise the emergency tool becomes a way around the rules that protected the ordinary workflow. Resolve the destination country through your application's trusted destination-validation process. “EU” is a policy grouping, not a single destination country, and a country allowlist is not proof of legal compliance.

Reserve the budget first.

The same gate must protect a resend. Claim the seller cooldown and budget reservation atomically before permitting a dispatch, using your chosen limits rather than guessed country prices. The following runnable Go example covers the next investigative step: fetch status for an already recorded message without authorizing another send. Save it as main.go and run it with the Go toolchain. Set INFRAI_API_ORIGIN to the service's HTTPS API origin without a path, INFRAI_API_KEY to your credential, and SMS_MESSAGE_ID to the actual identifier saved from the send response. The code prints the returned JSON without guessing undocumented response fields.

package main

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

func retryDelay(value string, fallback time.Duration) time.Duration {
    if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
        delay := time.Duration(seconds) * time.Second
        if delay > fallback { return delay }
    }
    if deadline, err := http.ParseTime(value); err == nil {
        if delay := time.Until(deadline); delay > fallback { return delay }
    }
    return fallback
}

func run() error {
    origin := strings.TrimRight(os.Getenv("INFRAI_API_ORIGIN"), "/")
    key, id := os.Getenv("INFRAI_API_KEY"), os.Getenv("SMS_MESSAGE_ID")
    base, err := url.Parse(origin)
    if err != nil || base.Scheme != "https" || base.Host == "" ||
        base.Path != "" || base.RawQuery != "" || base.Fragment != "" || base.User != nil {
        return fmt.Errorf("set INFRAI_API_ORIGIN to the trusted HTTPS API origin")
    }
    if key == "" || id == "" { return fmt.Errorf("key and message ID are required") }
    client := &http.Client{
        Timeout: 15 * time.Second,
        CheckRedirect: func(*http.Request, []*http.Request) error {
            return http.ErrUseLastResponse
        },
    }
    endpoint := origin + strings.Replace("/v1/sms/status/{id}", "{id}", url.PathEscape(id), 1)
    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequest(http.MethodGet, endpoint, nil)
        if err != nil { return err }
        req.Header.Set("Authorization", "Bearer " + key)
        resp, err := client.Do(req)
        if err != nil { return err }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil { return readErr }
        if resp.StatusCode == http.StatusTooManyRequests && attempt < 2 {
            time.Sleep(retryDelay(resp.Header.Get("Retry-After"), time.Second << attempt))
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return fmt.Errorf("status %d: %s", resp.StatusCode, body)
        }
        if !json.Valid(body) { return fmt.Errorf("status response is not JSON") }
        fmt.Println(string(body))
        return nil
    }
    return fmt.Errorf("retry budget exhausted")
}

func main() {
    if err := run(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

Polling grants no sending permission.

In production, eligibility, the cooldown claim, the spend reservation, and ownership of the notification attempt need an atomic application-side decision; two workers reading the same old values must not both acquire permission to send. Reconcile reservations against observed charges and set a deliberate policy for missing or stale accounting data. A guessed country price is a poor circuit breaker. The sample retries only a read, so it needs no write idempotency key; keep credentials and destination configuration under operator control, and retain returned status data in restricted records rather than public logs.

Keep notification identity separate from attempt identity. Replaying the same order event should resolve to the existing notification intent, while an authorized recovery attempt should be recorded as a new attempt beneath it. For write retries, use a stable idempotency key for that same operation rather than generating a new key on each network retry. This distinguishes recovering an interrupted request from authorizing another message.

How should SMS event notification alerts use delivery status polling?

Record the identifier returned by the send operation, then poll status and events to update the support view with states such as sent, delivered, failed, or undeliverable. Display when the observation was made. A stale “sent” label should not look like a fresh delivery confirmation, and the polling interval should follow the order workflow's tolerance for delayed evidence rather than the dashboard's refresh animation.

Back off on HTTP 429 responses, honor Retry-After when present, and bound retry work. Inspect response statuses and preserve useful error reasons in restricted operational records. Do not let a rate-limited poll trigger a replacement send: the application has lost a fresh observation, not established a delivery failure.

There are 2 recovery paths to distinguish. For a recoverable failure, authorize a resend after the same country, suppression, cooldown, and budget controls pass, preserving the order and template relationship. For a pending scheduled SMS that the product allows the seller to stop, request cancellation and observe the outcome before reporting it as canceled. Cancellation does not recall a message that has already reached its recipient.

An uncertain outcome needs investigation.

What page fired?

If a request times out after dispatch, keep the intent pending investigation and reconcile its recorded operation before authorizing another attempt. If support edits the message meanwhile, retain both revisions rather than overwriting the evidence for the first send. Where STOP or help replies are part of the product, poll inbound messages and propagate opt-outs into suppression before allowing further dispatch. Keep the reply-handling delay visible to the people operating that workflow.

Verify the repair, then preserve a rollback

Use a controlled destination and an approved template revision to follow one order from event creation through send acceptance and subsequent delivery observations. Verify that a duplicate order event does not authorize another send. Then exercise a blocked destination, an active seller cooldown, exhausted budget, suppression, and a recoverable failure. These are proposed acceptance checks, not claims of measured provider performance.

Test scheduled cancellation separately from resend. Introduce stale polling observations and a rate-limit response into the adapter test boundary, and check that the support screen neither invents delivery nor turns uncertainty into another SMS. For a Node.js order producer, preserve these same application decisions at the worker boundary; the Go example is a policy reference, not a language-specific guarantee about delivery.

Rollback should stop new dispatches for the affected workflow while preserving polling and investigation of existing attempts. Revert to a previously approved template revision only for newly authorized messages, retain the revision used by each existing attempt, and cancel eligible pending schedules through their recorded identifiers. Keep the marketplace's ordinary order view available so a seller can still find the order while customer support resolves the notification question.

The postmortem closes when support can answer which order, which copy, which attempt, and which delivery observation justified the next action. A green dashboard is insufficient evidence.

References

Top comments (0)