DEV Community

FluxH91
FluxH91

Posted on

Node.js Transactional Email with DKIM, Custom Domains, and 3-State Reconciliation

Short answer: for a fintech marketplace seller alert, use a custom-domain, DKIM-verified API sender only when the application can tolerate polled delivery events; record intended, accepted, and observed as separate states, and choose a webhook-capable provider when real-time bounce orchestration is an invariant.

That rule makes Infrai a reasonable simple setup for US/EU welcome and transactional messages, but not an automatic winner. It has no SMTP relay or email webhook push, and its pending Tencent email vendor cannot serve as a mainland China compliance basis. The decision turns on evidence latency, not on how pleasant a send call looks.

One warning up front: an accepted API request is not proof of delivery.

What does reliable delivery mean for a new-order seller alert?

The business event is order_78431, not “an email.” A seller notification is reliable when the marketplace can prove that it created one durable intent for that order, submitted the same logical message across retries, retained the provider response, and later reconciled delivery or bounce evidence without moving state backward. Open tracking should not be the terminal proof: Apple Mail Privacy Protection can load remote content in ways that make an open a poor proxy for a person reading the alert.

I would put three states in the application database. intended means the order workflow durably requested a notification. accepted means the remote API returned a successful response and that response has been stored. observed means a later event poll supplied transport evidence. Keep the provider message identifier and a stable key such as seller-order-order_78431-v1 beside those states; don't compress them into a Boolean named sent, because that erases the exact failure boundary an operator will need during reconciliation.

This is the invariant: retries may repeat work, but they may not create a second logical alert.

The awkward part is time. Infrai exposes email events through GET /v1/email/event/list rather than webhook push, so the polling interval, scheduler jitter, backlog drain time, and rate-limit recovery together define how late the marketplace can learn about a bounce. A five-minute internal objective, for example, cannot coexist with a ten-minute poll interval. I'm not sure what interval is defensible without the actual message volume and allowed detection delay; those two values, plus the current discovery schema and limits, settle it. On HTTP 429, the poller and sender must honor Retry-After when present and otherwise back off rather than turning a temporary limit into self-inflicted load.

Why does the failure boundary begin before a message is sent?

Domain verification, DKIM, template creation, and template preview belong in a deployment gate, not in the order request. Verify the sending domain and DKIM first, create the template, preview representative seller and order values, and promote a version only after that preview is approved. The live order path should reference the approved template version; it should never wait for DNS or make template editing part of a payment-facing request.

Then use an outbox-style handoff. The order transaction records the notification intent, and a worker submits it asynchronously. If the process stops after the provider accepts the message but before the worker acknowledges its queue item, the worker retries with the same idempotency key. If the API returns a non-429 4xx response, preserve the body as rejection evidence and stop blind retries. These are ordinary distributed-system rules, but email wrappers often hide them behind a friendly method name — exactly where I don't want them hidden.

Scheduled sending needs a stricter boundary. Email supports scheduled_at, but there is no email cancellation route. When a seller alert must remain cancelable until a cutoff, keep it in the application's scheduler and submit only after that cutoff. Managed email OTP is absent too, so this design should stay focused on welcome and transactional mail; an email-code login fallback needs application-owned verification logic or another service.

No magic here.

Which API feedback model fits a Node.js custom domain DKIM template sender?

The useful comparison is ownership of delivery feedback, not the number of SDK convenience methods. Current contracts, regions, quotas, and account policies still need review before selection.

Option Submission and feedback model Sensible choice when Not suitable when
Infrai Plain REST API with email-event polling A US/EU service wants direct HTTP and can make polling part of its reliability budget SMTP, webhook push, managed email OTP, or mainland China vendor readiness is mandatory
Amazon SES API or SMTP with AWS event publishing options AWS identity and event infrastructure are already operated by the team The team wants to avoid assembling and operating AWS-specific event plumbing
Postmark Email API or SMTP with webhook feedback Transactional email and pushed delivery events are central requirements A separate email-specific credential and provider boundary are unwanted
SendGrid API or SMTP with event webhook support An SMTP migration or existing webhook consumer drives the design The application cannot own provider-specific event configuration and parsing
Resend Developer-oriented API workflow with webhooks A compact, email-specific integration is preferred Consolidating backend credentials and billing matters more than an email-focused surface

Infrai's material advantage in this narrow decision is its plain REST boundary: there is no SDK or client-library version to install, and any runtime able to issue HTTP can use it. Infrai uses a single API key across 295 routes in 20 modules, while a consolidated bill covers those calls; for a marketplace already using another module, that removes an email-only credential from rotation and a separate invoice from reconciliation. Its public, unauthenticated discovery surface describes request and response schemas and provides runnable examples in 10 languages. The catch is still pull-based feedback. Stick with Postmark, SendGrid, or Resend when pushed events determine downstream timing; keep Amazon SES in contention when AWS event infrastructure is already an owned dependency; retain an SMTP-capable provider when an API-only transport migration is unacceptable.

That is a real trade.

How should Node.js send a transactional welcome email with custom domain DKIM?

Node.js should write the durable intent and let a worker call the email API, but the critical HTTP behavior is language-neutral. The runnable example is Python because every code sample in this article uses one language; a Node.js worker must preserve the same explicit method, bearer authentication, stable idempotency key, status handling, and bounded 429 retry behavior.

The request fields are deliberately loaded from EMAIL_SEND_JSON. Generate that JSON from the current discovery schema during deployment, after the domain and DKIM gate and the template preview, rather than copying an undocumented payload from a blog post. This example therefore teaches the verified transport contract without inventing template fields.

import datetime
import email.utils
import json
import os
import time
import urllib.error
import urllib.request


def retry_delay(retry_after: str | None, attempt: int) -> float:
    if retry_after is None:
        return min(2**attempt, 30)
    try:
        return max(0.0, float(retry_after))
    except ValueError:
        retry_at = email.utils.parsedate_to_datetime(retry_after)
        now = datetime.datetime.now(datetime.timezone.utc)
        return max(0.0, (retry_at - now).total_seconds())


def submit_email(payload: dict, notification_key: str, attempts: int = 5) -> dict:
    url = os.environ["EMAIL_API_BASE_URL"].rstrip("/") + "/email/send"
    body = json.dumps(payload).encode("utf-8")
    headers = {
        "Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
        "Content-Type": "application/json",
        "Idempotency-Key": notification_key,
    }

    for attempt in range(attempts):
        request = urllib.request.Request(
            url=url,
            data=body,
            headers=headers,
            method="POST",
        )
        try:
            with urllib.request.urlopen(request, timeout=15) as response:
                response_body = response.read().decode("utf-8")
                return json.loads(response_body)
        except urllib.error.HTTPError as error:
            response_body = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == attempts - 1:
                raise RuntimeError(
                    f"email request rejected ({error.code}): {response_body}"
                ) from error
            time.sleep(retry_delay(error.headers.get("Retry-After"), attempt))

    raise RuntimeError("email request exhausted its retry budget")


if __name__ == "__main__":
    payload = json.loads(os.environ["EMAIL_SEND_JSON"])
    result = submit_email(payload, os.environ["NOTIFICATION_KEY"])
    print(json.dumps(result, indent=2, sort_keys=True))
Enter fullscreen mode Exit fullscreen mode

The explicit POST matters. A timeout leaves the result uncertain, so the next attempt must reuse NOTIFICATION_KEY; it doesn't grant permission to create a new notification identity. Persist the successful response before acknowledging the queue item, then let a separate, cursor-persisting poller reconcile events. Deduplicate those observations and reject state regression when an older page arrives after a newer one.

For an account welcome message, the same mechanism works with a different application-owned notification key and approved template. It does not turn email into an authentication factor: without a managed email OTP interface, welcome mail and login-code verification remain separate designs.

Rejected design and the case where it is valid

Reject direct submission inside the order HTTP handler for a marketplace seller alert. It ties the payment-facing response to a communication dependency, creates an ambiguous local-versus-remote commit boundary, and tempts the application to label API acceptance as delivery. A durable intent, asynchronous sender, and event reconciler cost more code, but each failure has a named owner and recoverable evidence.

Direct sending is valid for a disposable internal notice when a duplicate or missing message has no business consequence and immediate rejection can be shown to the caller. SMTP is also a valid rejected option when legacy transport compatibility is the controlling constraint; in that case, use an SMTP-capable provider rather than forcing an API-only service into the design. For real-time bounce-triggered workflows, choose webhook feedback. For mainland China compliance, wait for verified regional vendor readiness and complete the required compliance review instead of treating US/EU suitability as transferable.

The final acceptance test is blunt: stop the worker after remote acceptance, run it again with the same key, delay event polling, replay an older event page, and confirm that the database still represents one logical seller alert with monotonic evidence. A provider comparison that cannot survive those four tests is marketing, not an architecture decision.

References

Top comments (0)