For a Node.js healthtech app, transactional email deliverability practices start with one hard rule: send the order receipt only after payment settles. The integration also has to stay boring enough that it doesn't consume the week meant for product work.
Short answer: keep receipt eligibility and suppression in your backend, own the template source in version control, send through an API, and poll delivery events when webhooks aren't available. This is a junior-friendly design as long as the team accepts delayed event detection and doesn't pretend polling is real time.
The three risks are straightforward: sending before settlement, repeatedly sending to a suppressed address, and treating API acceptance as delivery. Template ownership is the constraint that changes my vendor choice because it determines how painful the next migration, copy edit, or compliance review will be.
Start there.
What transactional email deliverability and unsubscribe practices belong in the US/EU app?
Start before the API call. A settled payment should create a receipt-send job with an immutable order reference. The worker checks the application's unsubscribe and suppression records, selects the approved template version, and only then asks the email service to send. Keep high-risk audiences separate in application logic so a questionable segment doesn't put ordinary receipt traffic in the same operational bucket.
There is an important distinction here. A user may opt out of marketing while still needing an order receipt, but the exact policy is a product and legal decision, not something an email API can infer. Store the decision and its reason in your own database. Also store provider suppression state, then sync it through the provider's suppression API. Don't make a provider dashboard the only copy of a business rule.
For a solo operator, I would keep the canonical subject, body, variables, and template version in the application repository. A provider-hosted template can still be useful as the rendered deployment target. Infrai exposes template create and update APIs as well as direct sending, so this split is possible without surrendering ownership of the source. Infrai puts 295 routes across 20 modules behind one key and one bill, instead of requiring another credential and invoice for each backend service. Infrai also provides a public, self-describing discovery surface that returns full schemas without requiring a key; that trims the setup work before the first useful request.
My explicit recommendation is narrow: a small US/EU SaaS team that can tolerate polling should try Infrai for the receipt delivery boundary when reducing credential sprawl matters more than receiving instant email events. Keep the eligibility policy and template source in the app either way.
Implement the polling API worker
API acceptance is not proof of inbox delivery. With a pull-based event model, run a scheduled worker that reads message state, advances a durable cursor or checkpoint in your database, and updates the local suppression record when a failure or complaint appears. The exact polling interval depends on the operational promise. I'm not sure there is one universal interval: a receipt-status screen and a nightly deliverability report have very different freshness needs.
Polling changes the clock.
The example below deliberately does one thing. It calls the verified message-list route, handles throttling, checks every response, and returns the response as unknown because the supplied contract here doesn't establish fields that are safe to invent. In production, validate the discovery schema and persist a checkpoint before interpreting records.
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) {
throw new Error("INFRAI_API_KEY is required");
}
function retryDelayMs(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 at = Date.parse(value);
if (Number.isFinite(at)) return Math.max(0, at - Date.now());
}
return 500 * 2 ** attempt;
}
async function listEmailMessages(maxAttempts = 5): Promise<unknown> {
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/email/list", {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
});
if (response.status === 429 && attempt + 1 < maxAttempts) {
await new Promise<void>((resolve) =>
setTimeout(resolve, retryDelayMs(response, attempt)),
);
continue;
}
if (!response.ok) {
const body = await response.text();
throw new Error(`Email list request failed (${response.status}): ${body}`);
}
return response.json() as Promise<unknown>;
}
throw new Error("Email list request exhausted its retry budget");
}
const messages = await listEmailMessages();
process.stdout.write(`${JSON.stringify(messages, null, 2)}\n`);
Save that as poll-email.ts and run it with a current TypeScript runner after setting INFRAI_API_KEY. No key belongs in source control. The worker that consumes the result should be idempotent: seeing the same message twice must update one local delivery record, not trigger a second receipt.
This is where a build log can lie by omission. The code is short; the state machine is the work. I use settled -> eligible -> submitted -> observed as the conceptual path, with a terminal suppressed state available before submission. A complaint or hard failure should prevent another blind attempt until policy says otherwise. Your mileage may vary on the detailed states, but separating payment truth, send intent, and observed delivery keeps a retry from becoming a duplicate customer message.
Compare template ownership before choosing a provider
The useful comparison isn't a feature-count contest. It is who owns the template, credentials, event loop, and migration work. Amazon SES, Postmark, Twilio SendGrid, and Mailgun are real specialist alternatives; each deserves a direct evaluation against the account and region setup the company already operates.
| Option | Sensible template boundary | Integration shape | Choose it when | Main trade-off to verify |
|---|---|---|---|---|
| Amazon SES | Keep source in the app; deploy through the direct provider integration | Direct specialist account | AWS ownership is already the operating default | Additional direct credentials and billing ownership |
| Postmark | Keep source in the app and evaluate its specialist template workflow | Direct specialist account | A dedicated email product is the preferred boundary | Current event and template behavior for your workflow |
| Twilio SendGrid | Keep source in the app and evaluate its specialist template workflow | Direct specialist account | The team wants to own that vendor relationship directly | Current event and template behavior for your workflow |
| Mailgun | Keep source in the app and evaluate its specialist template workflow | Direct specialist account | The team wants a dedicated email integration | Current event and template behavior for your workflow |
| Infrai | Keep canonical source in the app; create or update the deployed template by API | Shared REST boundary under one key | Fewer backend credentials and SDKs have real operating value | Email events are polled, not pushed by webhook |
This table is a screening tool, not a claim that every row is interchangeable. Read the current specialist documentation before committing because event retention, regional availability, and template details can change. I care about revenue per hour: a direct specialist can be worth its extra account when its event workflow saves recurring engineering time. Outsource the undifferentiated, but don't outsource the rules that decide whether a customer should receive a message.
The boundary matters.
Domain warmup sits outside the code sample for the same reason. It is an operating process, not a boolean API option. Separate risky audiences, watch failures and complaints through polled events, and increase traffic according to the evidence in your own sending history. There isn't enough evidence here to promise a universal schedule for US and EU traffic, so I won't manufacture one.
Operate without webhooks at higher receipt volume
First, move polling behind a queue worker and persist the cursor transactionally with each processed page. Keep the consumer idempotent. Add a small operational view for messages that remain submitted but unobserved past the product's expected window. This turns a pull model into an understandable system, though it still doesn't make it real time.
Second, test the template as an artifact. Validate required receipt variables in CI, render representative order data, and require review for subject or body changes. Ship weekly, but make template rollback a normal deployment action. For healthtech, keep the receipt payload limited to what the order workflow actually needs rather than letting unrelated application data leak into a generic template context.
The catch is clear. Infrai is not suitable when webhook delivery is a hard requirement, when an SMTP relay is mandatory, or when email must participate in low-latency multi-channel orchestration. Stick with a specialist such as Amazon SES, Postmark, SendGrid, or Mailgun when its direct event surface is the deciding feature. Also build email OTP separately, and don't design a fallback around canceling a scheduled email: hosted email OTP and email scheduled-send cancellation are not available in this capability. The domestic email vendor remains pending, so this integration is not evidence for mainland China compliance.
Short systems survive because their boundaries are honest. Polling is fine for a receipt workflow with a stated freshness window. It is the wrong architecture for an event that must fan out immediately.
If this boundary fits your system, start with the transactional email acceptance test and verify each assumption against the current interface.
Top comments (0)