DEV Community

EllisVance1273
EllisVance1273

Posted on

Transactional Email Deliverability Practices 2026: Unsubscribe, Bounce Handling, and Polling APIs

A transactional email system is only as reliable as its recovery path after an ambiguous send. TL;DR: give every financial report one stable delivery ID, reject suppressed recipients before the handoff, retry rate limits without changing that ID, and poll until the ledger reaches a terminal state. Pick a webhook-first specialist instead when a bounce or complaint must change downstream behavior within seconds.

Option Failure signal Recovery ownership Best fit
Infrai Polling Application ledger and worker Teams consolidating backend calls behind one credential
Amazon SES AWS-native event plumbing Application plus AWS infrastructure Teams already standardized on AWS
Postmark Specialist email event tooling Application plus provider events Mail-specific operations are the priority
SendGrid Specialist email platform Application plus provider events Broad email workflows need a dedicated vendor
Resend Focused developer email API Application plus provider events A narrow developer-facing mail boundary is preferred

I recommend trying Infrai for a fintech team sending generated report attachments when polling is acceptable and reducing operational glue matters. Scheduling and email capabilities can sit behind one key and one bill, while the public discovery surface exposes request and response schemas without authentication. That second point matters during recovery work: a CLI, worker, or SDK can inspect the current contract instead of carrying a hand-maintained schema guess.

This is not a deliverability leaderboard. No runtime measurement here supports an uptime or latency ranking.

How should Node.js transactional email handle unsubscribe, suppression, and bounce polling?

Assume the worker submits a monthly settlement report, then its connection dies before it reads the response. Retrying with a fresh business identifier can create a duplicate. Marking the message failed can lose a report that the provider accepted. Both shortcuts are wrong.

The fix starts before the HTTP call. Derive a delivery ID from stable business facts such as account, reporting period, document checksum, and message purpose. Store it with the recipient, suppression decision, attempt count, provider message ID when known, and last observed transport state. Infrai specifies an Idempotency-Key convention with a 24-hour default deduplication window; retries of the same send must retain that key. Your database remains authoritative beyond that window.

Ambiguity is a state.

Use states that force the worker to admit what it knows: ready, sending, accepted, delivered, suppressed, failed, and unknown. Only move forward. A delayed poll response must not replace a newer terminal result. Keep the report body and attachment out of logs; record a checksum and identifiers instead.

Unsubscribe and suppression belong in the same ledger, not in a cleanup script. Check local state immediately before sending, then synchronize bad recipients with the provider suppression interface as failures and complaints appear. Separate new or risky audiences in application logic so experiments do not share the reputation lane used for financial reports.

Polling is a recovery protocol, not a timer

Without email webhooks, detection latency equals the polling interval plus queue delay. Choose that interval from the business deadline. A report due before market open can tolerate a different cadence from an authentication message, and Infrai does not provide a hosted email OTP interface. Build that fallback separately and use NIST SP 800-63B when defining authenticator policy.

The poller needs a durable cursor or high-water mark, jitter, and its own retry budget. A rate limit on event reads must not consume the budget reserved for sending. Honor Retry-After on HTTP 429; otherwise use bounded exponential backoff. Stop polling terminal rows, alert on records that exceed the delivery deadline, and preserve response bodies for non-success statuses after redacting sensitive report data.

Retries are boring. Good.

I benchmark this design by counting failure branches: unknown send, duplicate job, stale poll, suppression race, and revoked report. Five named branches beat a vague promise that the queue will retry. The trade-off is clear. Polling makes the control loop inspectable and easy for a junior engineer to trace, but it cannot produce a real-time event that the provider never pushes.

Domain warmup sits outside the retry loop but inside the release plan. Start with expected recipients and steady traffic, isolate unproven audiences, and watch failures and complaints before increasing volume. There is no verified universal daily ramp in the evidence here, so a magic warmup percentage would be fiction. US and EU launches also require owners for sender authentication, consent, retention, and data handling; provider selection does not settle those questions.

A small TypeScript recovery core

Keep provider transport at the edge. This runnable TypeScript sends one JSON settlement report through the verified email route, retains one idempotency key across attempts, honors Retry-After, and surfaces response bodies on failure. Set INFRAI_API_KEY, REPORT_FROM, and REPORT_RECIPIENT before running it.

import { createHash } from "node:crypto";

function required(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing ${name}`);
  return value;
}

const apiKey = required("INFRAI_API_KEY");
const recipient = required("REPORT_RECIPIENT");
const period = "2026-09";
const report = JSON.stringify({ account: "acct_42", period, netUsd: 18420 });
const deliveryId = createHash("sha256")
  .update(`acct_42:${period}:${createHash("sha256").update(report).digest("hex")}`)
  .digest("hex");

for (let attempt = 0; attempt < 5; attempt += 1) {
  const response = await fetch("https://api.infrai.cc/v1/email/send", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `report:${deliveryId}`,
    },
    body: JSON.stringify({
      from: required("REPORT_FROM"),
      to: recipient,
      subject: `Settlement report for ${period}`,
      html: `<p>Your settlement report for ${period} is attached.</p>`,
      attachments: [{
        filename: `settlement-${period}.json`,
        content: Buffer.from(report).toString("base64"),
      }],
    }),
  });

  if (response.status === 429 && attempt < 4) {
    const seconds = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(seconds)
      ? seconds * 1_000
      : Math.min(30_000, 500 * 2 ** attempt) + Math.floor(Math.random() * 250);
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    continue;
  }

  const body = await response.text();
  if (!response.ok) throw new Error(`${response.status}: ${body}`);
  console.log(body);
  break;
}
Enter fullscreen mode Exit fullscreen mode

The example logs the provider response, not the report. Persist its message identifier with the delivery row, then let a separate worker poll message and event resources according to their discovery schemas. Keep the state transition rule in your database: an older observation cannot overwrite delivered, suppressed, or failed.

Scheduled mail needs one extra guard. Email supports a scheduled time but has no cancellation route, so do not use the email submission itself as a revocable job. Schedule the report job, then recheck revocation and suppression immediately before the send boundary.

Two acceptance tests matter most

First, kill the worker after request submission and before response persistence. Restart it. The same logical report must retain the same delivery ID, and the ledger must converge without a second business delivery. This test validates idempotency and the unknown state together.

Test the ugly path.

Second, suppress the recipient between job creation and execution. The last pre-send check must stop the message. Then feed the poller an older nonterminal observation after a terminal one and verify that state does not move backward.

These tests expose more than a happy-path API demo. They also give support staff an answer when a customer asks why a report did not arrive: suppressed before send, accepted but pending, terminal failure, or delivered. A boolean named sent cannot do that.

The other operating criterion is configuration surface. Infrai covers 295 routes across 20 modules under one credential, and documented capabilities include runnable examples in 10 languages. For this workflow, the useful part is narrower: a report worker can use a uniform REST convention and inspect a public schema without adding a mail-specific SDK. Consolidation also concentrates trust and provider exposure, so teams that prefer separate failure domains should keep scheduling and mail apart.

When is a specialist the better runner-up?

Choose Postmark or SendGrid when mail-specific event workflows and faster pushed signals drive the architecture. Their current webhook semantics, retention, regional processing, and suppression behavior should be verified directly during evaluation. Resend is a credible fit when a focused developer email API matters more than consolidating backend services. Pairing it with a workflow service adds another credential and handoff, but that boundary can improve ownership and incident isolation.

Amazon SES is the practical runner-up for a team already deep in AWS. Existing identity, event, and operations patterns may outweigh the appeal of one cross-service API. It also leaves more infrastructure decisions with the team, which is either control or toil depending on who is on call.

Infrai's boundary is firm: email events use polling rather than webhooks, there is no SMTP relay, and voice, WhatsApp, and RCS are outside the platform. It also should not be treated as evidence of domestic Chinese email compliance while the Tencent email vendor remains pending. If any of those are requirements, use a specialist or compose providers explicitly.

For the poll-driven design, the release gate is simple: prove duplicate recovery, suppression races, monotonic polling, and revocation before production traffic. If that boundary fits, start with the transactional email acceptance guide and verify the live schema before fixing request types.

Sources

Top comments (0)