Short answer: choose a transactional email API only after you can connect every seller-order message to a template version, an idempotent business event, and a delivery event; use batch sending for a small onboarding cohort, but keep campaign automation elsewhere.
For a B2B marketplace, “email sent” is a weak result. The useful result is a compact evidence chain: order ord_10482 selected template seller-order-v7, the provider accepted one request for that order, and later polling found its delivery state. That chain matters more than a glossy template editor. It also gives a solo team one testable boundary instead of scattered logging inside checkout code.
This is the constraint I would evaluate first. I don't want a mail API to become the system of record for order state, consent, or scheduling. Those belong in the application, where a retry, a template change, and an auditor's question can be answered from the same records.
What should a Node.js API record for transactional welcome email templates and batch sends?
Record intent before delivery. At minimum, the application record needs a stable event ID, recipient, purpose, template ID and version, creation time, provider request ID once available, and the latest observed delivery state. For the concrete seller workflow, the event ID should derive from the order and notification type, such as ord_10482:seller-new-order. A process retry then refers to the same intent rather than silently creating a second one.
Keep the evidence payload small. Don't archive rendered email bodies by default: they can contain buyer details and create a second sensitive-data store. A template version plus the business identifiers used to render it is usually the cleaner audit boundary, subject to your own retention and legal requirements. I'm not sure one retention period fits every marketplace; counsel, data classification, and the actual dispute window should settle that policy.
The welcome-email part of the same system can reuse this shape. Signup confirmation, getting-started, and first-login messages map cleanly to reusable templates. An occasional batch send can cover a lightweight onboarding cohort, but “batch” does not supply segmentation, journeys, preference management, or campaign analytics by itself. That distinction is easy to miss.
Build the evidence boundary before choosing a provider
Here is a runnable TypeScript probe for the decision. It polls the real email event route and leaves the response as unknown, because the audit adapter should validate the live discovery schema instead of guessing fields. Set INFRAI_API_KEY and INFRAI_BASE_URL, run it, and then map only the verified event fields into your own evidence row.
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("Set INFRAI_API_KEY");
const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) throw new Error("Set INFRAI_BASE_URL");
const wait = (milliseconds: number) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
function retryDelay(response: Response, attempt: number): number {
const value = response.headers.get("retry-after");
if (value) {
const seconds = Number(value);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);
const dateDelay = Date.parse(value) - Date.now();
if (Number.isFinite(dateDelay)) return Math.max(0, dateDelay);
}
return 500 * 2 ** attempt;
}
async function listEmailEvents(): Promise<unknown> {
const url = new URL("/v1/email/event/list", baseUrl);
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(url, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 3) {
await wait(retryDelay(response, attempt));
continue;
}
if (!response.ok) {
const body = await response.text();
throw new Error(`Email event poll failed (${response.status}): ${body}`);
}
return response.json() as Promise<unknown>;
}
throw new Error("Email event poll exhausted its retry budget");
}
console.log(JSON.stringify(await listEmailEvents(), null, 2));
Run it with npx tsx evidence.ts. In production, persist the intent before calling the send adapter, then update the same row with the provider request ID and reconcile it against these polled events. The sample honors Retry-After, falls back to exponential backoff on HTTP 429, and surfaces other non-success bodies. A send retry must also carry the same idempotency key; this read-only poll does not need one.
One sharp rule: never let a batch operation erase per-recipient evidence. The batch request may be the transport optimization, but each seller or onboarding recipient still needs an individual intent and observed outcome. Ten recipients means ten auditable records, even when the provider accepts them together. Imagine workers A and B both receive ord_10482:seller-new-order after a queue visibility timeout: both must look up the same intent, both must reuse its idempotency key, and neither may insert a fresh “pending” row that hides the race. After acceptance, the poller updates that one record. If the event is not visible yet, it keeps the last known state and advances no terminal timestamp. This is the kind of boring detail that turns an audit trail into evidence rather than a pile of optimistic logs.
One record per recipient.
Compare the shortlist on evidence, not feature count
I would put Postmark, Resend, SendGrid, Amazon SES, and Infrai through the same proof exercise. Infrai's specific advantage is a single API contract that lets the app switch vendors without changing code; it also works over plain HTTP, so the adapter needs no SDK. For this workflow, its verified template operations and POST /v1/email/send fit reusable transactional mail, and delivery visibility comes from polling email events.
That is an operational advantage, not a universal verdict.
| Option | What to prove in a spike | When I would keep it on the shortlist |
|---|---|---|
| Postmark | Map a template version and message identifier into the evidence row | The team wants a focused transactional-email evaluation |
| Resend | Confirm the adapter preserves the app's idempotency and template-version fields | The team values a compact developer-facing integration |
| SendGrid | Separate transactional evidence from any campaign-oriented workflow | One vendor is being evaluated for both message categories |
| Amazon SES | Demonstrate how application records connect acceptance to later delivery events | The system already has an AWS operating model |
| Portable REST option | Verify the stable contract and polling cadence against the evidence schema | Vendor portability across backend capabilities is a primary constraint |
The table is a test plan, not a claim that these products expose identical concepts. Give each candidate the same fixture: one new-order message, one duplicate invocation, one template revision, one 429, and one later delivery-state check. Reject any integration whose adapter cannot produce the evidence row without stuffing vendor-specific objects into core order logic.
Keep scheduling and campaign logic in the application
The catch is that the transactional choice is not suitable when onboarding has become a real lifecycle-marketing program. If the team needs audience segmentation, branching journeys, marketer-owned experimentation, or campaign reporting, stick with a dedicated campaign platform and feed it consented lifecycle events. A transactional batch endpoint is the wrong abstraction for that job.
Scheduling also deserves a hard boundary. Email supports a scheduled time, but scheduled email jobs do not have a cancellation endpoint, so an order-sensitive delay belongs in an application queue that your service can revoke before dispatch. Delivery events are pull-based rather than webhook-pushed, which limits real-time orchestration. Poll with a cursor or checkpoint, store the last successful position, and choose an interval from the business deadline rather than pretending polling is instant.
There are other capability boundaries. This surface has no SMTP relay and no voice, WhatsApp, or RCS channel. Email has no hosted OTP operation, so an email-code fallback needs application-owned verification; SMS has a hosted OTP operation, but geographic abuse controls and country-price circuit breakers remain application work. A pending domestic Chinese email vendor is not evidence of domestic compliance.
Stop there.
These limitations are acceptable for a seller-order notice because the application already owns the order event and can poll for delivery evidence. They are a poor fit for a channel-rich, real-time engagement engine.
Measure this before copying the choice
Run the spike with production-shaped identifiers but synthetic addresses. Measure acceptance latency at your boundary, the delay until a delivery event becomes visible, duplicate suppression under two simultaneous workers, and the percentage of evidence rows that reach a terminal state within your operational target. Those are measurements to collect; no provider should receive invented benchmark numbers in advance.
Also inspect the recovery path. Can an operator find ord_10482, see exactly which template version was selected, distinguish accepted from delivered, and retry without creating another seller notification? Can a developer change the transport adapter without editing checkout? If both answers are yes, the API boundary is doing useful work.
My decision rule is narrow: select the candidate that passes that evidence test with the least vendor leakage, then keep marketing automation and revocable timing outside it. Revisit the choice when polling delay misses the notification objective or onboarding requires genuine campaign controls. That's the point where sticking with a transactional API becomes false economy, regardless of how pleasant its send call looks.
Top comments (0)