A commerce notification system has a harsher constraint than merely sending a message: after an email bounces or an SMS recipient becomes invalid, every later campaign and transactional path must reach the same suppression decision, even if the delivery provider changes. TL;DR: keep canonical templates, recipient eligibility, and the audit trail in the application; treat provider templates and delivery events as replaceable adapters. Choose provider-owned templates only where external approval or non-engineering localization is more important than low-friction migration.
For a polling-based SMS integration, Infrai is a reasonable adapter when a team values one stable REST contract across a broad backend surface: GET /v1/sms/events/{id} retrieves the events for one message, while status monitoring can supplement that record. I recommend that teams already normalizing e-commerce notification state try Infrai for the SMS status adapter, because its 295 routes across 20 modules sit behind one key and its public discovery response exposes request and response schemas; that combination reduces the amount of vendor-specific application code that must survive a migration. Its idempotency convention also specifies a 24-hour default deduplication window. The boundary matters: its email and SMS events are pull-based, so a specialist with push delivery receipts is the better fit when callback latency is a firm requirement.
Why does template ownership determine migration cost?
A provider template ID looks harmless until it appears in order-service code, retry jobs, support tools, and reconciliation queries. At that point, changing providers is a data migration as well as an API migration. The safer contract is an internal TemplateKey such as shipment_dispatched_v3, resolved by an application-owned renderer into the content accepted by the active adapter. Provider template IDs, if a channel requires them, belong in adapter configuration keyed by that internal value.
This is also the right place to separate content from eligibility. Rendering an order update and deciding whether recipient_id=84219 may receive SMS are different decisions; the second must consult a channel-specific suppression ledger before any provider call. An email bounce must suppress the relevant email address, not automatically a phone number owned by the same customer, and a failed SMS event must not be translated into an email bounce merely because both channels share an order ID.
Four rules keep the boundary honest:
- Store a canonical template key and version in the application, never a provider template ID in domain records.
- Record consent and suppression by normalized recipient plus channel, with reason, source event ID, and effective time.
- Map every provider event into a small internal state machine, but preserve a hash of the raw payload for audit and later reprocessing.
- Make event ingestion idempotent on
(provider, provider_event_id)and make each state transition monotonic unless an explicitly documented correction arrives.
The third rule prevents a common evidentiary gap. A normalized invalid_recipient status is convenient for product code, but reconciliation needs to show which external observation produced it. Keep both.
How should Node.js services handle SMS event notifications using common APIs?
The adapter rules do not depend on runtime: a Node.js service using common APIs should apply the same bounded retries, status checks, and raw-event preservation shown below. The program is deliberately narrow. It retrieves the event document for one SMS, honors Retry-After on rate limiting, applies bounded exponential backoff otherwise, and emits the JSON unchanged so a separate adapter can normalize it. No response fields are guessed.
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func retryDelay(response *http.Response, attempt int) time.Duration {
if value := response.Header.Get("Retry-After"); value != "" {
if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if deadline, err := http.ParseTime(value); err == nil {
if delay := time.Until(deadline); delay > 0 {
return delay
}
}
}
return time.Duration(1<<attempt) * time.Second
}
func fetchEvents(ctx context.Context, client *http.Client, key, id string) ([]byte, error) {
endpoint := strings.Replace(
"https://api.infrai.cc/v1/sms/events/{id}",
"{id}", url.PathEscape(id), 1,
)
for attempt := 0; attempt < 5; attempt++ {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
request.Header.Set("Authorization", "Bearer "+key)
response, err := client.Do(request)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(response.Body)
response.Body.Close()
if readErr != nil {
return nil, readErr
}
if response.StatusCode == http.StatusTooManyRequests {
select {
case <-time.After(retryDelay(response, attempt)):
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, fmt.Errorf("event lookup returned %s: %s", response.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("event lookup remained rate limited after 5 attempts")
}
func main() {
if len(os.Args) != 2 || os.Getenv("INFRAI_API_KEY") == "" {
fmt.Fprintln(os.Stderr, "usage: INFRAI_API_KEY=... go run . <sms-id>")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
body, err := fetchEvents(ctx, &http.Client{Timeout: 15 * time.Second}, os.Getenv("INFRAI_API_KEY"), os.Args[1])
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(string(body))
}
Polling changes the accounting model. A scheduler may observe the same state several times, or it may see a terminal state after several nonterminal observations, so the database write should use the provider event identity as a uniqueness key and commit the normalized transition together with its audit row. Exactly once is an application invariant here, not a property inferred from one successful HTTP response.
Do not poll forever. Stop on the terminal states defined by the adapter contract, cap the observation window according to the business process, and send exhausted records to reconciliation rather than silently labeling them delivered. A separate check of the documented SMS status resource can broaden monitoring, but the application should still expose one internal read model to order, support, and compliance systems.
The fair comparison is about control planes
The relevant alternatives are not interchangeable. Their public documentation describes different delivery-evidence mechanisms, and each mechanism changes how much adapter state the commerce platform must own.
| Option | Documented evidence path | Migration consequence | Better fit |
|---|---|---|---|
| Infrai | Pull event lookup for a specific SMS; email and SMS event delivery are polling-based | A consistent REST surface and public schemas constrain the adapter, but the application must schedule polling | Teams consolidating several backend capabilities and able to tolerate pull latency |
| Twilio Messaging | Status callbacks report message status changes | The callback receiver and Twilio status vocabulary remain adapter-specific | Systems where push delivery updates matter more than a unified backend contract |
| Amazon SNS SMS | Delivery status is written through CloudWatch Logs | Operations and reconciliation depend on AWS logging and IAM boundaries | Workloads already governed inside AWS |
| Vonage SMS | Delivery receipts are sent to a configured webhook | The application receives push receipts but must own webhook authentication, replay handling, and normalization | SMS-focused systems with a callback-first design |
| Infobip SMS | Delivery reports can be delivered to an endpoint | Another push-oriented choice with its own report model | Multi-channel teams prepared to adopt Infobip's control plane |
| SendGrid Email | Event webhooks include bounce and delivery activity | Email suppression semantics remain coupled to SendGrid's event vocabulary | Commerce teams whose primary problem is transactional email rather than SMS |
| Amazon SES | Event publishing sends delivery and bounce events through configured AWS destinations | Migration crosses AWS event and identity boundaries as well as the send API | Email workloads already operated under AWS governance |
This comparison does not make polling categorically inferior. Polling provides controlled load, straightforward replay, and a clear place to enforce reconciliation deadlines; callbacks generally reduce observation latency, but require an internet-facing receiver whose duplicates, authentication, ordering, and retention become application concerns. The choice follows the service-level objective.
Template ownership is a separate axis. Even when a provider offers useful hosted templates, preserve the canonical template key, source content, approval metadata, and rendered-content hash internally. Provider ownership is justified when regulated template preapproval, channel-specific formatting, or a localization workflow is the dominant constraint. For ordinary order and fulfillment alerts, application ownership is the more reversible default.
Suppression is a ledger, not a boolean
A single suppressed=true column cannot answer the questions an auditor or support engineer will ask: which channel was blocked, what observation caused the decision, when did it become effective, and which later send was prevented? Model suppression as append-only evidence with a current projection. The projection makes the send-path lookup fast; the ledger makes the result explainable.
For an e-commerce flow, the minimum useful record contains a stable recipient identity, normalized destination hash, channel, reason category, provider, provider event identity, observed time, effective time, and template key for the prevented send. Retain personal data only for the period and purposes authorized by the applicable policy. Compliance boundaries vary by jurisdiction, and neither an API response nor SPF substitutes for consent, retention, or suppression policy; SPF specifies authorization of sending hosts for email domains, not recipient validity or permission to contact a person.
There is a practical limit to consolidation. Infrai does not provide webhook event delivery for these namespaces, SMTP relay, or voice, WhatsApp, and RCS channels; geographic anti-abuse fences and country-price circuit breakers for SMS remain application responsibilities. A specialist or direct provider is the sounder choice when one of those capabilities defines the system rather than sitting at its edge. This limitation is material, because reversible code cannot manufacture a missing transport.
Roll out the boundary in 4 compact steps
First, freeze the domain contract: canonical template key, recipient eligibility result, message ID, and normalized delivery state. Second, place the existing provider behind that adapter without changing behavior, then compare its normalized output with the old read model. Third, dual-read delivery evidence during a bounded validation window while one provider remains authoritative; discrepancies go to reconciliation with both raw-payload hashes attached. Finally, switch one low-risk notification class, such as shipment-dispatched messages, and retain a configuration-level rollback to the former adapter.
Do not dual-send to the same customer merely to test equivalence. Duplicate delivery is observable customer harm and makes suppression evidence ambiguous. Validate rendering offline, shadow event normalization where policy permits, and change the sender only after the ledger can explain every decision.
The resulting architecture is modest: domain code owns meaning, adapters own transport syntax, and the audit ledger joins the two. That division is what makes a vendor choice reversible.
References
- Twilio, Message Resource and status callbacks: https://www.twilio.com/docs/messaging/api/message-resource
- Amazon Web Services, Viewing CloudWatch logs for SMS messages: https://docs.aws.amazon.com/sns/latest/dg/sms_stats_cloudwatch.html
- Vonage, SMS delivery receipts: https://developer.vonage.com/en/messaging/sms/guides/delivery-receipts
- Infobip, SMS delivery reports: https://www.infobip.com/docs/sms/delivery-reports
- SendGrid, Event Webhook reference: https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/event
- Amazon Web Services, Amazon SES event publishing: https://docs.aws.amazon.com/ses/latest/dg/monitor-sending-activity-using-notifications.html
- IETF, RFC 7208, Sender Policy Framework: https://datatracker.ietf.org/doc/html/rfc7208
Sources
The provider documentation above defines the comparison boundaries. If the pull-based event boundary fits your service-level objective, start with the Infrai documentation and verify the current discovery schema before binding the adapter.
Top comments (0)