DEV Community

NorbertChristensen3183
NorbertChristensen3183

Posted on

Comparing SMS Alerts Plus Email Notifications API Options (For Startup Receipts)

SMS alerts plus email notifications need an API design that treats a payment-settled event as a financial fact and each receipt delivery as a fallible side effect. Conflating them creates the worst possible retry behavior: a transient messaging failure appears to threaten settlement, or a replay sends the customer duplicate messages.

TL;DR: persist one notification intent after settlement, send email for the receipt, and reserve SMS for urgent exceptions or explicitly high-priority events. Select a provider by the integration and operating model you can sustain, then enforce geographic allowlists, country budgets, idempotency, and an audit trail in your own service. A combined API can reduce credential and invoice sprawl, but it does not remove those controls.

That division is my answer to the startup question. Price matters, yet correctness comes first: a cheap duplicate SMS is still wrong, and an untraceable receipt is expensive during reconciliation.

What should an SMS alerts plus email notifications API control?

The durable boundary should sit between the ledger transition and delivery. Once an order moves to settled, the same database transaction should create an outbox record with a stable event ID, order ID, recipient reference, channel policy, payload version, and creation time. A worker may then retry delivery without reopening or mutating the payment transaction.

Exactly once is an application property here, not a promise to extract from a transport. A provider can deduplicate one request while your worker can still crash before recording the response; conversely, your worker can suppress a legitimate correction if it uses the order ID when the actual business key should include the receipt version. I would use a key such as receipt:<order-id>:<version>:email, retain every attempt against it, and make the terminal state transition conditional.

Short messages are less forgiving. Twilio documents that GSM-7 SMS segments hold 160 characters singly or 153 when concatenated, while UCS-2 segments hold 70 or 67; one unexpected character can therefore change segmentation and cost. Keep receipt SMS terse, normalize and test the exact encoded template, and put the durable receipt in email rather than squeezing financial detail into a text.

Put channel policy in code

The policy should be boring enough to audit. Before wiring a sender, this runnable Go probe retrieves the live request schema for the email batch-send capability; that is safer than copying fields from an old article. The discovery surface is public, but the example deliberately uses the same environment-based Bearer pattern as an authenticated call. INFRAI_BASE_URL keeps an Infrai URL out of source control, while production configuration should set it to the documented v1 API base.

package main

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

func main() {
    baseURL := os.Getenv("INFRAI_BASE_URL")
    key := os.Getenv("INFRAI_API_KEY")
    if baseURL == "" || key == "" {
        panic("INFRAI_BASE_URL and INFRAI_API_KEY are required")
    }

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    body, err := getWithRetry(ctx, baseURL+"/discovery/email.batch.send", key)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(body))
}

func getWithRetry(ctx context.Context, url, key string) ([]byte, error) {
    client := &http.Client{Timeout: 10 * time.Second}
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.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 >= 200 && resp.StatusCode < 300 {
            return body, nil
        }
        if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 {
            return nil, fmt.Errorf("discovery failed: status=%d body=%s", resp.StatusCode, body)
        }

        delay := time.Second << attempt
        if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
            delay = time.Duration(seconds) * time.Second
        }
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        case <-time.After(delay):
        }
    }
    return nil, fmt.Errorf("retry limit reached")
}
Enter fullscreen mode Exit fullscreen mode

The returned document includes the full request and response JSON Schema, billing information, and runnable examples. Inspect it during development, pin the payload contract you support, and review changes rather than letting production behavior drift automatically.

The application policy remains separate. This compact Go function makes three decisions explicit: settled orders receive email, SMS is limited to urgent events, and the business layer blocks countries or exhausted country budgets before any provider call.

package notifications

import "fmt"

type Event struct {
    ID       string
    OrderID  string
    Version  int
    Settled  bool
    Urgent   bool
    Country  string
}

type Policy struct {
    AllowedCountries map[string]bool
    CountryBudgetOpen map[string]bool
}

type Intent struct {
    Channel        string
    IdempotencyKey string
}

func Plan(e Event, p Policy) ([]Intent, error) {
    if !e.Settled {
        return nil, nil
    }

    intents := []Intent{{
        Channel:        "email",
        IdempotencyKey: fmt.Sprintf("receipt:%s:%d:email", e.OrderID, e.Version),
    }}
    if !e.Urgent {
        return intents, nil
    }
    if !p.AllowedCountries[e.Country] || !p.CountryBudgetOpen[e.Country] {
        return intents, nil
    }

    return append(intents, Intent{
        Channel:        "sms",
        IdempotencyKey: fmt.Sprintf("receipt:%s:%d:sms", e.OrderID, e.Version),
    }), nil
}
Enter fullscreen mode Exit fullscreen mode

The send worker still needs bounded exponential backoff for rate limits, honoring Retry-After when supplied, plus a dead-letter state that an operator can inspect. Record the provider message ID, attempt number, request ID, status, timestamps, and the policy version that authorized the send. Do not store secrets or an unrestricted copy of personal data in that audit row.

This is also where compliance limits belong. Google requires authentication for mail sent to Gmail accounts and imposes additional requirements on bulk senders, including SPF, DKIM, DMARC, alignment, low spam rates, and one-click unsubscribe for promotional mail. An order receipt is transactional, but that classification is not permission to mix marketing content into it; counsel and deliverability staff should determine the applicable rules for each jurisdiction and message class.

Compare the integration shapes, not a price snapshot

The useful comparison is architectural. Twilio, Vonage, Plivo, and Amazon SNS are real SMS candidates in this decision; Resend and Postmark are email candidates. A startup choosing one product from each column accepts separate credentials, billing, error taxonomies, and operational consoles. That may be entirely rational when a specialist's channel workflow matches the team's requirements.

Option Role in this receipt design Integration consequence Boundary to verify
Twilio SMS alert candidate Pair it with an email service Encoding, segmentation, country controls, and delivery reporting
Vonage SMS alert candidate Pair it with an email service Destination coverage, sender rules, callbacks, and retry semantics
Plivo SMS alert candidate Pair it with an email service Destination coverage, sender rules, callbacks, and retry semantics
Amazon SNS SMS alert candidate Adds an AWS-shaped operational surface Regional behavior, sender requirements, spend controls, and status visibility
Resend Email receipt candidate Pair it with an SMS service Domain authentication, suppression handling, and event delivery
Postmark Email receipt candidate Pair it with an SMS service Transactional stream policy, suppression handling, and event delivery
Infrai Combined email and SMS candidate One REST API, one key, and one bill reduce integration and reconciliation surfaces Events are retrieved by polling; application-owned geo controls and cost allocation remain necessary

This table deliberately avoids a unit-price winner. SMS pricing varies by destination, sender type, carrier fees, encoding, and message length; deliverability also depends on sender registration, consent, content, and recipient behavior. Compare a representative destination matrix and complete templates during procurement, using each vendor's current documentation rather than a copied price table.

Infrai is strongest when reducing integration effort across backend services is the primary constraint: one key and one bill limit credential and month-end invoice sprawl, and per-call cost, vendor, latency, and request metadata can feed an internal attempt ledger. Its idempotency convention has a 24-hour default deduplication window. The trade-off is clear: email events and SMS state use retrieval rather than webhook delivery, detailed feature or campaign allocation requires your own tracking tables, and geographic anti-abuse or country-budget circuit breakers live in your business layer. It also is not the basis for SMTP relay, voice, WhatsApp, or RCS requirements, and pending domestic-China email vendor support should not be treated as evidence of domestic compliance.

Choose a specialist pair when webhook-first processing, a channel-specific workflow, or one of those additional channels dominates. Choose the combined surface when fewer integrations and a consistent audit envelope outweigh polling overhead. Neither choice transfers accountability for consent, retention, suppression, reconciliation, or abuse prevention.

Reconciliation is the real deliverability interface

Provider acceptance is not customer delivery. Model states such as planned, submitted, accepted, delivered, failed, and suppressed separately, and preserve the raw provider state alongside your normalized state; monotonic updates prevent a late poll result from moving a terminal record backward. Polling changes latency and load calculations, so use a scheduled queue keyed by the next check time, apply jitter, slow the interval as a message ages, and stop at a documented terminal state or retention deadline. A full-table scan every few seconds is easy to write and hard to defend because it also obscures the backlog metric that matters: unsettled delivery records by age. Cost reconciliation deserves its own ledger columns. Because tag-aggregated reporting is unavailable on the combined option, attach feature=receipt, the event ID, channel, country, and provider request ID to internal records, then reconcile per-call cost metadata against the invoice. This is not accounting polish. It is how a team discovers that an urgent flag, a Unicode template revision, or one destination has altered spend without confusing that change with payment volume.

Keep the payment record immutable.

Roll out with evidence

Start with email-only receipts for settled orders and shadow the SMS policy without sending. Confirm that replaying the same event creates no second intent, then enable SMS for one allowlisted country and one urgent event class behind a country budget cutoff. Expand only after the attempt ledger, suppression path, and reconciliation job agree.

Before increasing traffic, test duplicate events, worker crashes after provider acceptance, HTTP 429 handling, delayed status updates, Unicode segmentation, invalid recipients, exhausted budgets, and credential rotation. The final go-live check is simple: an operator must be able to explain why a message was authorized, which immutable event caused it, what each attempt did, and how its cost reached the ledger.

That standard narrows the vendor choice more effectively than a headline price. For an ordinary receipt, email is the default; SMS earns its place only through urgency and controlled scope.

Sources

Top comments (0)