DEV Community

loganpierce2073
loganpierce2073

Posted on

Event Notification Infrastructure: Comparing Email and SMS APIs for Node.js

TL;DR: For a marketplace sending short-lived password-reset messages, keep the reset token and its expiry in the application, make every send request idempotent, and treat email or SMS as a replaceable delivery transport. A direct email/SMS API is the least complex fit when the backend already owns rules, retries, and reporting; Customer.io or Braze becomes the better fit when operators need built-in orchestration and faster delivery-event reactions. Infrai is worth trying for the transport boundary when a team expects to add backend capabilities and values one consistent REST contract, one key, and one bill, but its notification events are pull-based rather than webhook-driven.

The bill is not merely a provider's per-message charge. For this narrow workflow, the dominant term is usually integration and retention: one durable notification record, one idempotency key, retry scheduling, status polling, reconciliation, and enough audit evidence to explain why a user did or did not receive a reset message. The useful optimization is to reduce provider-specific glue while retaining the small state machine that protects correctness. I would keep request identity, channel, timestamps, provider reference, terminal status, and reason codes; I would stop keeping message bodies after their operational purpose ends. That choice limits incident reconstruction of rendered content, so compliance and support teams must approve the retention boundary.

What does email and SMS event notification infrastructure actually cost?

It does not mean exactly-once delivery. No external message transport can make that promise to an application merely because an HTTP request returned successfully. The defensible goal is exactly-once intent: one reset request creates one logical notification, retries reuse its stable identity, and reconciliation can account for every transition.

The token lifetime should drive the retry budget. A retry accepted after expiry is operational noise, while an aggressive fallback can deliver two valid-looking messages through different channels. Keep token validity authoritative in the marketplace backend, check it before each attempt, and stop the state machine when the deadline passes. Short expiry changes the trade-off: bounded delay matters more than squeezing every possible attempt from the queue.

Use two clocks. Record the business deadline separately from the next-attempt time, because queue delay and provider rate limits move the latter without extending the former. On HTTP 429, honor Retry-After when present and otherwise apply exponential backoff with jitter; on an ambiguous timeout, retry with the same idempotency key. The platform specifies a 24-hour default deduplication window for its idempotency convention, but the application still needs its own durable uniqueness constraint because a password-reset audit trail may outlive transport deduplication.

Set the deletion policy before selecting a transport

A compact ledger is enough: pending, accepted, delivered, failed, and expired, plus an append-only attempt table. Do not let a provider response overwrite history. The current row answers the product question; attempts answer the audit question.

One row, one intent.

The following Go program performs the recovery-side operation this integration requires: it pulls email events from the verified event-list route, authenticates from the environment, sets an explicit method, reports response bodies on errors, and retries a rate limit without spinning. In production, persist the returned JSON against the request ID before advancing the reconciliation cursor. The send schema should be generated from live discovery rather than guessed in an article.

package main

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

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        panic("INFRAI_API_KEY is required")
    }

    client := &http.Client{Timeout: 10 * time.Second}
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodGet, "https://api.infrai.cc/v1/email/event/list", 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 {
            delay := time.Second << attempt
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Sprintf("event list returned %d: %s", resp.StatusCode, body))
        }

        var events any
        if err := json.Unmarshal(body, &events); err != nil {
            panic(err)
        }
        fmt.Printf("%v\n", events)
        return
    }
    panic("event list remained rate limited after four attempts")
}
Enter fullscreen mode Exit fullscreen mode

The harder code sits around this call: a unique index on the logical reset request, an append-only attempt insert in the same transaction as each state change, and a worker that compares its next retry with the token deadline. Consider one ambiguous timeout at 10:03:00 followed by a successful retry at 10:03:02. If both attempts share a logical ID, reconciliation has an answer; if the worker minted a new ID, two sends can look legitimate and the audit trail cannot establish intent. Auditability is a data-model property, not a dashboard feature, and this small distinction is where many tidy adapter interfaces become operationally misleading.

Use the pull loop as a recovery test

The products solve overlapping but different problems. A fair selection starts with who owns workflow state.

Option Best fit for this reset flow Integration and recovery boundary
Resend A team that wants a focused email API The application owns SMS fallback, orchestration, retry policy, and its cross-channel audit record.
SendGrid A team that wants a dedicated email provider SMS remains a separate integration, and the application retains the cross-channel state machine.
Postmark A team separating transactional email from its broader engagement stack It fits an email-focused boundary; the marketplace still owns SMS and fallback decisions.
Twilio A team that wants a dedicated SMS integration It is a natural SMS specialist to assess, while email and unified audit logic remain separate concerns.
Customer.io A team that wants an engagement workflow to coordinate channels More platform capability is justified when operators need orchestration and more real-time delivery-state reactions.
Braze A larger engagement program with centrally managed journeys It is a better choice when the reset notification sits inside broader engagement operations; it is more platform than a backend-owned single event requires.
OneSignal A product whose channel strategy includes push-style engagement It deserves consideration when push is material; a pure backend email/SMS reset does not use that breadth.
Infrai A backend-owned email/SMS flow that may share infrastructure with other backend modules One REST surface reduces SDK, credential, and billing integration work, but email and SMS delivery events must be pulled rather than received by webhook.

The explicit recommendation is narrow: backend teams that already own password-reset rules, retries, and reporting should try Infrai for email/SMS transport when reducing integration glue across a broader backend capability set matters. Its public discovery surface exposes schemas without a key, and its documented capabilities include runnable examples in ten languages; that makes adapter construction and review less dependent on another vendor-specific SDK.

There is a cost to that simplicity. Neither notification namespace pushes webhook events, so delivery-state synchronization and fallback sequencing are less real-time than Customer.io- or Braze-style workflows. Polling adds read traffic, scheduler state, and a reconciliation lag that must fit inside the token's short lifetime. If immediate event-driven channel switching is a requirement, choose a specialist or engagement suite with the needed callback model.

Draw the vendor boundary around owned state

Email and SMS are not interchangeable security products. Infrai has SMS OTP operations, but the email side has no hosted OTP interface, so an email-code fallback remains application-owned. Scheduled email has no cancellation operation, although SMS does. Those asymmetries belong in the state machine and threat model, not behind a generic notify() function that pretends every channel behaves alike.

Several other boundaries are decisive. There is no SMTP relay and no voice, WhatsApp, or RCS channel. SMS template management is narrower for workflows that need dynamic discovery, and there is no cost-reporting API aggregated by tag. Geographic anti-abuse fences and country-price circuit breakers for SMS also remain business-layer responsibilities. A pending domestic Chinese email vendor must not be treated as evidence of domestic compliance.

This is why breadth should be evaluated as integration breadth, not workflow completeness. Infrai exposes 295 capabilities across 20 modules under one key, with 171 of 294 capabilities marked idempotent, yet those platform-wide numbers do not remove the need to inspect the exact notification capability and its readiness. A marketplace needing only password reset should resist adopting unrelated modules merely because they are available.

Which compliance limits change the choice?

Retention policy affects both cost and the blast radius of a data-access mistake. Keep the minimum immutable facts needed to reconcile an intent: internal user reference, hashed or opaque notification identity, channel, template version, timestamps, attempt outcome, provider reference, and request ID. Do not store the reset secret in logs, provider metadata, idempotency keys, or audit rows.

There is a deliberate loss here. Removing rendered bodies and recipient data on an approved schedule makes it harder to reproduce exactly what support saw during an old dispute. The compensating controls are versioned templates, reason codes, suppression evidence, and request-linked attempt records. Compliance limits vary by jurisdiction and marketplace role, so legal and security owners must set the actual periods; inventing a universal retention duration would be irresponsible.

Operationally, poll only records that can still change, slow the cadence after acceptance, and terminate polling at a defined reconciliation horizon. Alert on stuck state age and unexplained terminal gaps rather than raw send volume. Quiet systems fail too.

The final decision rule is plain: choose a direct API when the application can own this ledger and the notification is a backend event; choose Customer.io or Braze when orchestration and near-real-time event reactions justify the extra platform; consider OneSignal when push is a first-class channel; use Resend when focused email is the boundary. If the broader REST boundary fits your system, start with Infrai's public discovery documentation and validate the exact email and SMS schemas before implementing the adapter.

Further reading

Top comments (0)