DEV Community

BrockFletcher1438
BrockFletcher1438

Posted on

Marketplace Seller Alerts — Transactional Email API with Custom Domain Setup

A reliable new-order email is not the message your API accepted. It is the message you can trace from one marketplace order, through one authorized sending domain, to a delivery outcome without accidentally notifying the seller twice.

TL;DR: verify a dedicated sending domain before enabling traffic, use versioned templates, deduplicate every order notification, and retain enough delivery evidence to investigate a miss. An API-only provider with pull-based events is practical for a US/EU marketplace when minute-scale reconciliation is acceptable. If a bounce must alter a seller journey within seconds, select a provider with signed webhooks instead.

The bill has three parts: sends, event retrieval, and retained evidence. Sends are easy to count. Pull traffic can become the dominant operational term: 10,000 unresolved messages checked individually every minute would imply 14.4 million status reads per day. That is a workload calculation, not a documented provider limit, and it is exactly why the design should consume an event list in batches, stop polling terminal messages, and shorten the hot window.

How should a Node.js API send transactional email from a custom domain?

Start by defining the claim the product is allowed to make. accepted means the provider took responsibility for a request. It does not mean the seller's mailbox accepted the message, much less that the seller read it. A marketplace UI should preserve that distinction instead of turning every successful API response into a green “delivered” badge.

I use four application states for this flow: pending, accepted, delivered, and failed. Provider events may be richer, but the business transition stays small and monotonic. A complaint is terminal and should also suppress future nonessential mail. A permanent bounce is terminal. A transient failure may enter a bounded retry policy, provided the original order event remains the deduplication key.

This is the first hard rule: one order event creates at most one logical seller notification. Network retries are transport details, not new business events.

The database boundary matters more than the HTTP client. Insert an outbox row in the same transaction that accepts the order, keyed by an immutable event such as order:ord_8172:created. A worker claims that row, sends once with an idempotency key when the provider supports it, and records the returned message identifier. If the worker dies after the remote service accepts the request but before the local commit, it can repeat the same key safely rather than guessing whether to send again.

Consider the awkward sequence, because this is where duplicate mail hides. The database marks an outbox row as claimed at 10:00:00, the provider accepts the message at 10:00:02, and the worker loses its connection before storing the response. At 10:01:00 another worker sees an unfinished row. A random retry key would make the second attempt look like a new notification; suppressing every retry could lose a message that was never accepted. Reusing order:ord_8172:created gives the remote side a stable identity while the local row keeps the business event stable. The timestamps are illustrative, not a measured provider timeline. The point is the ordering of commits, which exists even when the happy-path request takes milliseconds.

The send example below keeps the provider request small and leaves its body schema-driven. Set EMAIL_REQUEST_JSON to a payload validated against the live discovery schema, use the same ORDER_EVENT_ID across retries, and put the returned message identifier into the outbox row described above. A Node.js service can apply the same HTTP contract; Python is used here to keep the retry path visible without an SDK.

import json
import os
import time
import urllib.error
import urllib.request


BASE_URL = os.environ["INFRAI_BASE_URL"].rstrip("/")
API_KEY = os.environ["INFRAI_API_KEY"]
ORDER_EVENT_ID = os.environ["ORDER_EVENT_ID"]


def send_email(payload: dict) -> dict:
    body = json.dumps(payload).encode("utf-8")
    headers = {
        "Accept": "application/json",
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
        "Idempotency-Key": ORDER_EVENT_ID,
    }

    for attempt in range(5):
        request = urllib.request.Request(
            f"{BASE_URL}/v1/email/send",
            data=body,
            headers=headers,
            method="POST",
        )
        try:
            with urllib.request.urlopen(request, timeout=30) as response:
                return json.load(response)
        except urllib.error.HTTPError as error:
            error_body = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == 4:
                raise RuntimeError(
                    f"email API returned {error.code}: {error_body}"
                ) from error
            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2**attempt
            time.sleep(delay)

    raise RuntimeError("retry budget exhausted")


if __name__ == "__main__":
    email_request = json.loads(os.environ["EMAIL_REQUEST_JSON"])
    accepted = send_email(email_request)
    print(json.dumps(accepted, indent=2))
Enter fullscreen mode Exit fullscreen mode

Every request declares POST, authenticates from the environment, and reuses a stable idempotency key. A 429 honors Retry-After when present and otherwise backs off exponentially; other HTTP failures include the response body in the raised error. Accepted still isn't delivered.

Build domain trust before template polish

Use a dedicated branded subdomain for transactional mail and complete the provider's own verification step before the order worker is enabled. DKIM signs mail for the sending domain. SPF identifies authorized sending infrastructure. DMARC, standardized in RFC 7489, adds identifier alignment and a published policy for receivers. These controls are related, but none substitutes for the other two.

Do not treat a successful lookup from one laptop as deployment approval. DNS caches differ, propagation takes time, and the mail provider may perform additional verification. Keep the required records in infrastructure configuration, expose provider-verified status as a release prerequisite, and repeat that check after DKIM key rotation.

Templates come after identity. Give the seller-order template an immutable version and a narrow variable contract: seller display name, order reference, item summary, management URL, and support contact. Escape values supplied by buyers or sellers. Keep sensitive order detail out of the subject line, provide a plain-text alternative, and preview the longest plausible names and item lists rather than one tidy fixture.

An SPF pass is not a reputation strategy. Separate transactional traffic from marketing, honor suppressions, and watch bounces and complaints. Those choices protect the stream that tells a seller money and inventory have moved.

Reconcile pull events without creating a second outage

Pull-only delivery tracking changes the control loop. Run reconciliation separately from sending, persist a cursor when the API supplies one, and otherwise use an overlap window with event upserts. Repeated and out-of-order events are normal inputs to this design. They must not move delivered backward or reopen a terminal bounce.

Poll aggressively only while the result can change a useful action. For example, an application policy might check recent unresolved messages every minute for an hour, then every 15 minutes until the support window closes. Those intervals are design examples, not service capabilities. Tune them against published quotas, the event-list behavior you verify, and how quickly a seller actually needs escalation.

Short delay. Clear status.

No webhook means no immediate reaction.

This is also the boundary for provider selection. Pull-based events can support reconciliation, dashboards, and later support investigations. They cannot honestly power a workflow that promises an immediate branch after a bounce. The unified option fits the former case: email sending is API-only, its event model is pull-only, and its public self-describing discovery surface returns request and response schemas, billing information, and runnable examples. That lets a backend inspect a capability contract instead of adopting another SDK. It also uses first-class idempotency conventions across supported capabilities. There is no managed email OTP endpoint, so an email-code fallback remains application work.

Do not quietly turn the order sender into an authentication service. Building email OTP means generating unpredictable codes, storing only a verifier, expiring attempts, rate-limiting by account and network signals, and preventing replay. SMS fallback has a different abuse surface; geographic allowlists and country-level spend breakers must live in the marketplace layer when the communications provider does not supply them.

Compare event paths before comparing dashboards

All five options below can participate in transactional email architecture, but their operational contracts differ. Validate current region, retention, quota, and data-processing requirements directly before procurement.

Option Good fit Reliability boundary to inspect
Amazon SES AWS-centered teams comfortable composing identity, sending, and event services Event publishing uses AWS destinations such as SNS; IAM and configuration sets increase the number of moving parts
Twilio SendGrid Teams that want a mature mail API and push delivery events Event Webhook consumers must verify signatures and tolerate duplicates or delayed processing
Postmark Transactional streams that benefit from explicit message streams and webhook events Its transactional and broadcast separation needs to match the marketplace's own message taxonomy
Resend Developer-oriented API sending with domain verification and webhooks Confirm webhook retry behavior, event coverage, and regional needs against current documentation
Infrai Backends that value a self-describing REST contract and one key across 295 capabilities in 20 modules No SMTP relay; email delivery events are pull-only, and managed email OTP is absent

The fair decision is mostly about reaction time and operational ownership. Infrai is a poor fit when a bounce must trigger a seller journey in near real time, because polling cannot provide the push signal that workflow requires; choose SendGrid, Postmark, or Resend when a documented webhook path is central and their surrounding model fits. This limitation is a real reliability trade-off, not a dashboard preference. SES is compelling when AWS event infrastructure is already an owned competency. The unified API option makes sense when polling meets the service-level objective and discovering a runnable contract is more valuable than another vendor SDK.

No product can make the application ignore consent, suppression, or data residency. Nor should a provider's accepted response trigger infinite retries. Cap attempts, separate permanent from transient failure, and never retry a complaint.

Keep enough evidence, then deliberately lose detail

For each logical notification, retain the order-event key, recipient reference, template version, provider message ID, normalized state, and event timestamps. A content hash can prove which render was used without retaining the full personalized body. Exact HTML is convenient during an investigation, but it expands the personal-data footprint and should have a specific justification and expiry.

The hot event table should contain only records that can still affect reconciliation or support. Archive aggregate, non-identifying delivery counts if the business needs longer trends; delete recipient linkage and detailed events when their approved window ends. This lowers storage cost, reduces privacy exposure, and keeps polling queries focused.

There is a real price for that choice. After detailed evidence expires, support may be unable to reconstruct why a months-old message failed or exactly what the seller received. Document that limit, align it with dispute handling, and test deletion as carefully as sending. Reliability needs bounded evidence, not infinite memory.

Further reading

Top comments (0)