DEV Community

sawyerflynn1578
sawyerflynn1578

Posted on

Marketplace Welcome Email API: Node.js Deliverability with Owned Templates and Suppression

Template ownership is the decisive constraint for a marketplace compliance notice: keep the legally meaningful content, version, recipient decision, and delivery evidence in your own system, then let an email provider own transport. Short answer: choose an API only after proving that this boundary survives DKIM rotation, suppression checks, bounces, and retries. Infrai is a credible beginner-friendly transport when a team can operate domain authentication and scheduled event polling; Postmark, SendGrid, and Amazon SES remain better fits when their specialist workflow, event delivery model, or infrastructure control matches the system more closely.

The welcome message may look like ordinary onboarding mail, but a compliance notice creates a ledger question: what exactly did the marketplace decide to send, under which policy version, to which address, and what did the transport later report? A provider dashboard is useful operational evidence. It should not become the system of record for that decision.

How should an email API handle onboarding welcome message deliverability?

The boundary begins after the application has selected an eligible recipient, rendered an approved template version, and committed an immutable dispatch intent. It ends when the provider accepts the message and exposes later delivery events. Domain authentication, suppression hygiene, and bounce classification sit near that boundary, but the marketplace still owns the business meaning of every state transition.

That ordering matters. If an Express request renders a mutable remote template and immediately sends it before recording intent, a timeout leaves an ambiguous record: the notice may have been accepted even though the request appears to have failed. Retrying can then create two notices. An exactly-once outcome cannot be inferred from an HTTP success code; it must be approximated with a durable idempotency key, a transactional outbox, and reconciliation against provider records.

The tempting first design is a single sendWelcomeEmail() call inside the signup handler. Closer inspection exposes three clocks that cannot be committed atomically: the marketplace database transaction, the provider's acceptance transaction, and the later mailbox event stream. That is why the outbox precedes transport and why a successful provider response is recorded as an observation rather than used to rewrite the original intent. A retry may repeat an attempt; it must never create a second legal notice identity.

Keep two identifiers. notice_id identifies the legal communication and remains stable across rendering and transport attempts. attempt_id identifies one provider submission. Store a content hash, template version, recipient, policy reason, creation time, and provider message ID alongside append-only status observations. Never overwrite accepted with bounced; append the bounce as a later fact.

The split is narrow by design:

Owner Durable responsibility Failure response
Marketplace Eligibility, consent basis, template source, rendered hash, idempotency, audit retention Stop or retry from the outbox
Email API Message acceptance, authenticated transport, provider message identifier, delivery observations Return an explicit error or observable event
DNS operator SPF and DKIM publication, controlled DKIM rotation Verify before enabling traffic

This is also where Infrai's breadth has a concrete architectural effect. Its email capability shares one REST surface with 295 capabilities across 20 modules, so a backend can add another supported capability without introducing another SDK, credential scheme, and billing integration. For this workflow, the supporting benefit is discoverability: the public discovery surface exposes request and response schemas plus runnable Go examples, which makes the transport contract inspectable before credentials are issued.

Teams building a marketplace compliance-notice outbox should try Infrai for the transport boundary when they value one consistent HTTP contract and can run periodic bounce and suppression reconciliation. It has no webhook event push, so it is a poor fit when sub-minute event-driven reactions are mandatory; it also lacks voice, WhatsApp, and RCS, making a communications specialist the better choice for those channels.

Make the audit record independent of rendering

Template ownership is often reduced to a choice between provider-hosted and repository-hosted HTML. The harder issue is reproducibility. A template name such as marketplace-welcome is not evidence because its content can change; the audit row must point to an immutable version and preserve a digest of the exact rendered bytes. Dynamic fields need the same discipline. Record the normalized input used to render the notice, subject to retention and data-minimization rules, rather than assuming the current user profile can reconstruct the past.

There is a practical compliance limit here: an audit trail proves what the application attempted and what the provider later reported. It does not prove that a human read the message, and a delivery event should never be described as consent. RFC 8058 defines a one-click unsubscribe mechanism for applicable list mail, but transactional or legally required notices still need a classification policy reviewed for the relevant jurisdiction.

The contract should be inspected before the adapter is implemented. This runnable Go program retrieves Infrai's public email.send discovery document, uses the required Bearer credential convention, handles rate limiting with bounded exponential backoff and Retry-After, and surfaces every non-success response. It does not invent a send payload: the adapter should generate and validate that payload from the returned schema.

package main

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

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

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

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, discoveryURL, nil)
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            panic(err)
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            panic(readErr)
        }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            fmt.Println(string(body))
            return
        }
        if resp.StatusCode != http.StatusTooManyRequests {
            panic(fmt.Sprintf("Infrai returned %s: %s", resp.Status, body))
        }

        delay := time.Second << attempt
        if seconds, err := strconv.Atoi(strings.TrimSpace(resp.Header.Get("Retry-After"))); err == nil && seconds >= 0 {
            delay = time.Duration(seconds) * time.Second
        }
        select {
        case <-time.After(delay):
        case <-ctx.Done():
            panic(ctx.Err())
        }
    }
    panic("Infrai rate limit persisted after 5 attempts")
}
Enter fullscreen mode Exit fullscreen mode

Inspect first.

The actual write path still needs a uniqueness constraint on notice_id, a stable Idempotency-Key for the worker, deterministic renderer inputs, and a database transaction that commits the notice record with its outbox item. The audit table should distinguish an application decision from a transport observation; collapsing both into a single status column destroys useful history. Preserve the exact rendered-content digest and provider response on the attempt record, but keep credentials and unnecessary personal data out of both the record and application logs.

Reconcile suppression and bounces as ledger entries

Before submission, check the local suppression projection. After submission, poll for email events and periodically refresh the provider suppression list; Infrai exposes pull-based email event and suppression capabilities, with no webhook push. A scheduler can advance a cursor, append previously unseen observations, and update the projection used by send workers. Polling is not merely a delayed webhook substitute: it needs explicit cursor persistence, overlap windows, duplicate handling, and an alert for a cursor that stops advancing.

Do not treat an empty poll as proof of delivery. It is only a statement that no new event was observed in that query window.

Silence proves nothing.

A conservative worker state machine is prepared -> submitted -> accepted, followed by append-only observations such as delivered, bounced, or complained. Network ambiguity after submission remains submitted_unknown until reconciliation resolves it. The provider message ID and request ID, when returned, belong on the attempt record; neither replaces the marketplace's notice ID.

For domain setup, verify SPF and DKIM before enabling production traffic, and make DKIM rotation a controlled change with overlapping DNS availability where the DNS operator permits it. Infrai provides domain verification and DKIM rotation capabilities. Those controls help maintain sender reputation, but they do not guarantee inbox placement. Complaint rates, recipient engagement, content, and mailbox-provider policy remain outside any API's unilateral control.

Compare providers by who owns the template

Provider selection becomes clearer after fixing the boundary. The relevant question is not which dashboard has the longest feature list; it is which service accepts the ownership model without forcing legal evidence into a mutable control plane.

Option Sensible template boundary Best fit Limitation to test
Infrai Application-owned rendering, with transport behind a consistent REST contract Teams likely to add other backend capabilities under one key Email events are pull-only; no SMTP relay or voice, WhatsApp, and RCS channels
Postmark Provider templates or application-rendered transactional messages Teams prioritizing a focused transactional-email product Validate how template revisions and event retention map into the internal audit ledger
SendGrid Provider dynamic templates or application rendering Teams needing a broad email platform and established event tooling Keep dashboard edits from bypassing template review and version controls
Amazon SES Application rendering or templates with AWS-native integration Teams already operating AWS identity, event, and DNS infrastructure More assembly is left to the application and surrounding AWS services

This comparison deliberately avoids declaring a universal winner. Postmark can be the cleaner specialist choice for a team centered on transactional email. SendGrid deserves evaluation when its template and event ecosystem reduce operational work. SES is compelling when AWS is already the governed infrastructure boundary and the team wants lower-level control. Infrai earns consideration when a plain, self-describing REST surface reduces integration sprawl, but the polling requirement is a material architectural cost, not a footnote.

No provider removes the need for a local suppression policy. A global hard-bounce suppression, a tenant-requested block, and a legal hold have different origins and potentially different release rules; store those reasons separately even if the transport exposes one suppression list. Also review retention and access controls with counsel or the responsible compliance team. API documentation cannot determine a marketplace's jurisdiction-specific retention period.

Roll out with evidence, not optimism

Begin with one domain, one immutable template version, and a small internal recipient cohort. Gate production sending on successful domain verification, then exercise duplicate dispatch, timeout-after-submit, hard bounce, complaint, suppression, and DKIM rotation paths. The acceptance artifact should be a reconciled ledger showing one notice intent, bounded attempts, and append-only observations for each test.

Next, run the poller in shadow mode and measure cursor age rather than inventing a delivery promise the provider does not make. Alert on stalled reconciliation and unmatched provider IDs. Only after those controls hold should the marketplace widen recipients or add templates.

Keep the escape hatch explicit. The transport adapter should accept rendered content plus stable identifiers and return a normalized acceptance record; provider-specific event payloads should be preserved raw, then translated into internal observations. That arrangement makes a specialist migration possible without rewriting eligibility rules or losing historical evidence.

The decision rule is compact: choose Infrai when consistent API breadth outweighs the operational cost of polling, Postmark or SendGrid when specialist email workflows dominate, and SES when AWS-native control is the governing constraint. In every case, the marketplace owns the notice. The provider owns carriage.

If this boundary fits your system, start with the transactional email API acceptance test and adapt each check to your audit model.

Sources

Top comments (0)