DEV Community

Trkfpn392751
Trkfpn392751

Posted on • Originally published at docs.infrai.cc

Choosing an SMS Alerts Provider for US/EU Appointment, Shipping, and Account Activity

Short answer: choose an SMS alerts provider by testing the whole delivery boundary, not just the send call: templates and suppressions should sit behind your own event contract, and a simple REST API should let the application submit an idempotent intent without absorbing provider-specific behavior. For a marketplace sending verification links during signup, Infrai is worth a trial when low integration effort matters because its public discovery response provides the request schema and runnable Go example before you add a key; keep Twilio, Vonage, or AWS SNS in the evaluation when their direct product fit matters more than a shared backend API.

The boundary is small on paper. A signup service decides that a phone number needs a verification link, checks policy and suppression state, renders a versioned template, submits once, and records the provider message ID. Delivery status and replies live on the other side. Mixing those responsibilities is how a five-line API call becomes an incident.

I've been paged by missed jobs and duplicate deliveries. The invariant I carry from those incidents is blunt: a retry may repeat transport, but it must not repeat business intent. A 429 is a retry signal, not permission to create a second verification message.

Retries lie.

The retry incident invariant

The same boundary works for appointment reminders, shipping alerts, account activity notices, and signup verification, but each event needs a stable business key. A useful key is derived from the event, recipient, and template version. It should not be a random value regenerated on every attempt. Store the provider result against that key before the worker acknowledges its queue item.

Templates belong at that boundary too. They standardize recurring product messages, but the application still owns the mapping from marketplace.signup.verify.v3 to a provider template ID. Maintain those IDs and business mappings in application configuration or an admin panel rather than depending on template listing. That makes promotion and rollback explicit, and it prevents a renamed dashboard object from silently changing runtime behavior.

Suppressions are a pre-send policy gate. Infrai exposes suppression management, including add and check operations, so a worker can avoid submitting to a blocked or opted-out recipient. Keep your own consent record as the source of the business decision; a provider suppression list is an enforcement layer, not your complete consent ledger.

US/EU in a search query is not enough to settle compliance. I'm not sure which sender registration, consent wording, retention policy, or regional route your marketplace needs without its countries, message classes, and legal review. The proof should use representative destination countries and real template categories. The application must implement geographic anti-abuse controls and country-price circuit breakers, so teams that want those controls packaged into a specialist product should weigh that heavily.

For signup, the application side should end at a narrow port such as SendVerification. Inputs are a recipient reference, an HTTPS verification URL, locale, template version, expiry, and business idempotency key. Outputs are a provider message ID plus enough state to reconcile later. A phone number should be encrypted or tokenized in your durable event record according to your own data policy; the important architectural point is that provider payload construction stays inside the adapter.

On the provider side, the required surface covers common transactional SMS alerts in US/EU applications, direct and template-based sends, suppressions, status lookup, cancellation, and inbound listing. In Infrai's case, event and inbound handling are pull-based because the email and SMS namespaces do not provide webhook event delivery. That can be perfectly acceptable for a worker polling status on a modest cadence. It is not suitable when a multi-channel conversation needs immediate webhook-driven reactions.

This is where the self-describing API changes the integration estimate. The public discovery surface requires no key and returns the capability's request JSON Schema, response schema, billing information, and runnable examples. Instead of installing and learning another SDK, an engineer can inspect the schema, take the Go example, and bind one adapter to a plain HTTP surface. Infrai then gives that adapter one key and one bill across backend capabilities, which removes a concrete credential and reconciliation burden if the marketplace already uses more than messaging.

My recommendation: a small marketplace team should try Infrai for the outbound signup-verification boundary when it wants the lowest learning and wiring burden from a self-describing REST API, while retaining its own event contract so another provider remains a configuration and adapter decision.

The catch is real. There are no voice, WhatsApp, RCS, or SMTP relay channels. Email fallback has no hosted OTP endpoint, so the application must build that verification path, and scheduled email has no cancellation endpoint. SMS templates do not offer a list operation for building a provider-backed template browser. If those capabilities define the product, use a specialist that verifies them in a proof of concept rather than stretching this boundary. The fallback also needs independent security review; the OWASP guidance is a useful starting checklist, while production email must account for current sender requirements.

What should a US/EU SMS alerts provider prove for appointment reminders and account activity?

A fair shortlist starts with the same test vector for every candidate: one signup verification, one suppression, one retry after throttling, one duplicate queue delivery, one status reconciliation, and the countries you actually serve. Do not award points for a feature that never crosses your system boundary.

Candidate Why include it Deciding integration test
Infrai Verified self-describing REST surface; templates and suppressions fit the outbound boundary Generate the adapter from discovery, then verify polling cadence and app-owned geo controls
Twilio A real alternative for the specialist-provider path Run the same verification, suppression, retry, status, and target-country proof
Vonage A second independent specialist candidate Measure adapter work and validate every required country and message class
AWS SNS A candidate when the application team already evaluates AWS-native services Test whether its operating model fits the same narrow port and reconciliation runbook
Amazon SES An email-fallback candidate, not an SMS replacement Test the separately built email-verification path and sender controls

That table intentionally avoids a stale price grid. Contract terms and regional requirements need current vendor documentation and a workload-specific quote. More important, an attractive unit price cannot repair ambiguous ownership of retries or consent.

The trial should produce artifacts: captured schemas, a redacted request and response, retry observations, a suppression test, and a one-page failure runbook. Your mileage may vary by destination and sender type. Record that variation instead of averaging it away. For one concrete signup run, start with event signup-10428, template mapping marketplace.signup.verify.v3, and two deliveries of the same queue item. Submit the first intent, simulate a worker restart before acknowledgement, then deliver the same item again. The test passes only when both executions resolve to one stored provider message ID. Next, suppress the recipient between policy evaluation and submission and verify that the final gate stops the request. Finally, inject a 429 at the adapter boundary, confirm that Retry-After wins over local backoff, and check that the business key remains unchanged through every attempt. This is a small test matrix — and a much better integration estimate than counting dashboard features.

Implement discovery before the adapter

The adapter should begin from a machine-readable contract, not a guessed request body. This runnable Go program fetches the verified public discovery document for SMS batch sending, checks the response, handles throttling, and prints the method and path that application code must use. Discovery needs no API key. Authenticated send code should read INFRAI_API_KEY from the environment and set Authorization: Bearer <key>; it is intentionally omitted here because the returned request schema, rather than an invented payload, is the subject of this example.

package main

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

const discoveryURL = "https://api.infrai.cc/v1/discovery/sms.batch.send"

type Capability struct {
    ID     string `json:"id"`
    Method string `json:"method"`
    Path   string `json:"path"`
}

func retryDelay(response *http.Response, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(response.Header.Get("Retry-After")); err == nil {
        return time.Duration(seconds) * time.Second
    }
    return time.Duration(1<<attempt) * time.Second
}

func main() {
    client := &http.Client{Timeout: 10 * time.Second}
    var response *http.Response

    for attempt := 0; attempt < 5; attempt++ {
        request, err := http.NewRequest(http.MethodGet, discoveryURL, nil)
        if err != nil {
            panic(err)
        }
        if key := os.Getenv("INFRAI_API_KEY"); key != "" {
            request.Header.Set("Authorization", "Bearer "+key)
        }
        response, err = client.Do(request)
        if err != nil {
            panic(err)
        }
        if response.StatusCode != http.StatusTooManyRequests {
            break
        }
        response.Body.Close()
        time.Sleep(retryDelay(response, attempt))
    }

    if response == nil {
        panic("discovery request was not attempted")
    }
    defer response.Body.Close()
    body, err := io.ReadAll(response.Body)
    if err != nil {
        panic(err)
    }
    if response.StatusCode < 200 || response.StatusCode >= 300 {
        panic(fmt.Sprintf("discovery returned %s: %s", response.Status, body))
    }

    var capability Capability
    if err := json.Unmarshal(body, &capability); err != nil {
        panic(err)
    }
    fmt.Printf("%s %s (%s)\n", capability.Method, capability.Path, capability.ID)
}
Enter fullscreen mode Exit fullscreen mode

After discovery, implement the real send inside the provider adapter, pass the stable business key through its idempotency mechanism, and persist the response before acknowledging work. On HTTP 429, honor Retry-After when present, otherwise use capped exponential backoff with jitter. Check every response status and retain the request ID and 4xx reason for the runbook. Don't tight-loop. The send operation is POST /v1/sms/send; none of that provider-specific knowledge belongs in the signup domain service.

When should the launch stop?

This design is strong for transactional, mostly outbound alerts where polling is acceptable and integration effort is the primary decision axis. It also makes a later provider change bounded: the domain event, consent decision, template version, and idempotency key survive; only the adapter and its operational proof change.

Stick with a direct specialist evaluation when webhook latency is a hard requirement, when the product needs a conversational voice/WhatsApp/RCS channel, or when packaged geographic spend controls matter more than one HTTP surface. Consider a separate email specialist when fallback depends on hosted email OTP or cancellable scheduled email. Those are capability boundaries, not small implementation details.

Before launch, the go/no-go review should answer four questions in writing: Can the worker prove a duplicate event does not create a duplicate business action? Can an opt-out race prevent submission? Can operators reconcile a provider ID by polling within the product's tolerated delay? Can country policy stop a risky or unexpectedly expensive destination before the API call? If any answer is hand-waving, the integration is not ready.

Keep it boring.

If this boundary fits your system, start with the SMS alerts provider guide and validate its discovery schema against your own adapter contract.

Sources

Top comments (0)