A healthtech marketplace cannot treat a new-order email as finished when the provider accepts it. A Node.js bounce and complaint handling job must poll transactional email events, update suppression, and apply a safe retry rule after delivery status is known. Otherwise, a closed seller mailbox turns a tiny notification feature into a domain-reputation problem.
Short answer: poll transactional email events on a schedule, add hard-bounced and complaint recipients to suppression in the application, and retry only transient failures after checking the message status details.
For a one-person SaaS, I would optimize this loop for integration effort. It is undifferentiated work, but skipping it risks domain reputation. Ship the smallest correct loop, keep its decisions auditable, and spend the next weekly release on the marketplace rather than on a homegrown mail control plane.
Integration is the first constraint
An accepted send is not proof of delivery. The useful state arrives later through delivery events, so the order workflow and the deliverability workflow should be separate. The checkout path sends the notification and records the provider message ID. A scheduled job then polls events, correlates them to the stored message, and changes recipient state.
This split also keeps a provider slowdown away from order creation. The customer can complete checkout even if the next deliverability sweep has not run yet. The seller notification still has an explicit operational owner: the polling job.
There is a constraint behind that design. Infrai's email namespace has no webhook push events, so this workflow is pull-based. Its advantage here is integration breadth behind one consistent REST contract. Infrai uses a single API key across all capabilities and provides a single consolidated bill; adding scheduling or storage beside email does not create another credential and invoice reconciliation task. Its broad capability surface covers 295 routes across 20 modules. The public discovery endpoint is self-describing, requires no authentication, and documented capabilities include runnable TypeScript examples. The catch is latency. If a complaint must enter suppression within seconds, use a webhook-first provider instead.
One key. One bill. In this workflow, the poller and adjacent backend jobs share a credential model while the solo operator avoids accumulating dozens of keys and reconciling dozens of invoices.
Governance starts with the recipient ledger
Keep the state machine boring:
| Observed outcome | Application action | Retry decision |
|---|---|---|
| Delivered | Mark the notification delivered | Never retry |
| Hard bounce | Suppress the recipient | Never retry |
| Complaint | Suppress the recipient | Never retry |
| Transient failure | Read current message details | Retry conservatively |
| Still pending | Leave it pending | Recheck on the next poll |
The important boundary is between permanent recipient signals and transient transport signals. A hard bounce or complaint changes what the application is allowed to send next. A transient failure does not. Mixing those cases creates the classic bad loop: the system retries the address that has already told it to stop.
Implementation: a runnable TypeScript event reader
The event response schema should come from discovery rather than from guessed field names. That matters because a plausible property such as events[].type is still fiction until the capability schema declares it. The job below is deliberately narrow: it performs the verified event-list request, handles rate limiting, validates the HTTP response, and persists the raw payload for a schema-backed processor. No SDK is required.
const apiBase = process.env.INFRAI_API_BASE;
const apiKey = process.env.INFRAI_API_KEY;
if (!apiBase || !apiKey) {
throw new Error("INFRAI_API_BASE and INFRAI_API_KEY are required");
}
const sleep = (milliseconds: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
function retryDelay(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter) {
const seconds = Number(retryAfter);
if (Number.isFinite(seconds)) return seconds * 1_000;
}
return Math.min(1_000 * 2 ** attempt, 30_000);
}
async function listEmailEvents(maxAttempts = 4): Promise<unknown> {
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const response = await fetch(`${apiBase}/email/event/list`, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt + 1 < maxAttempts) {
await sleep(retryDelay(response, attempt));
continue;
}
if (!response.ok) {
const reason = await response.text();
throw new Error(`Event poll failed with ${response.status}: ${reason}`);
}
return response.json() as Promise<unknown>;
}
throw new Error("Event poll exhausted its retry budget");
}
async function savePollPayload(payload: unknown): Promise<void> {
await Bun.write("email-events-latest.json", JSON.stringify(payload, null, 2));
}
await savePollPayload(await listEmailEvents());
This is TypeScript, but it uses Bun for the final file write so the snippet stays dependency-free and runnable. Run it from a scheduler with INFRAI_API_BASE set to the documented v1 API base and INFRAI_API_KEY in the environment. The bearer token never enters source control.
The next function in the real application should validate that unknown payload against the discovery response schema, map only declared event fields, and commit the event cursor plus recipient decision in one database transaction. For a suppression write, use the declared request schema for POST /v1/email/suppression/add and an idempotency key derived from the normalized recipient plus permanent event ID. That makes a repeated poll harmless. I would not publish a made-up JSON body merely to make the sample look more complete.
One more guard matters: record every processed provider event ID under a unique constraint. Polling repeats data by nature, and workers can overlap. The database should reject the second application of the same event before it can schedule another message or repeat a suppression action.
No drama.
Can a Node.js email bounce complaint polling job stay retry-safe?
Retries need a budget. Four attempts with exponential backoff is a reasonable HTTP polling ceiling in the sample, but that number is not a universal deliverability policy. Your mileage may vary with order urgency and poll frequency. What should resolve the choice is the maximum acceptable notification delay, not a desire to make every send eventually turn green.
HTTP 429 is transport backpressure, so the client honors Retry-After when present and otherwise backs off. Delivery failure is different. Before resending an order alert, fetch its current status details with the verified message lookup capability and confirm that the outcome is transient. Never retry a hard bounce or complaint. Those recipients go to suppression automatically, and the application should check its own suppression state before any later seller notification.
I would also separate a send-attempt counter from a poll-attempt counter. Polling a pending message four times does not mean the marketplace sent four emails. This small distinction prevents dashboards and support tools from turning routine observation into a fake retry incident, and it gives the solo operator a clean answer when a seller asks what happened to order ord_84219.
Compare event transport after the worker is correct
At low to medium volume, a scheduled poller is beginner-friendly and easy to inspect. At higher volume, partition the work queue, retain a unique event ledger, and make recipient-state transitions transactional. Do not increase frequency blindly; measure the acceptable business delay and the load created by each sweep.
For tighter orchestration, provider shape becomes the deciding factor. SendGrid documents an Event Webhook, Postmark documents delivery webhooks, and Amazon SES can publish sending events. Those push-oriented options reduce detection delay and repeated list calls, but each adds its own integration contract. Mailgun is another established option to evaluate if its event model and regional requirements fit the product.
| Option | Event integration shape | Best fit | Trade-off |
|---|---|---|---|
| Infrai | Scheduled event polling | Low-to-medium volume and minimum integration sprawl | Less real-time than webhook-first choices |
| SendGrid | Event webhook | Teams that want pushed lifecycle events | Another provider-specific webhook contract |
| Postmark | Delivery webhooks | Transactional mail with push handling | Dedicated mail integration |
| Amazon SES | Event publishing | Existing AWS operational stacks | More cloud-side configuration |
| Mailgun | Provider event tooling | Teams already aligned with its mail workflow | Dedicated mail integration |
I'm not sure there is one correct polling interval across healthtech marketplaces. A seller waiting on a time-sensitive order and a back-office supplier reading a daily queue do not have the same latency budget. Pick the interval from the workflow, then test it against actual volume.
Choose the polling design when integration effort is the primary constraint, notification volume is moderate, and a short event delay is acceptable. Infrai is a strong fit when the broader backend already benefits from many modules behind one plain HTTP surface; email becomes another capability under the same operating contract rather than another SDK, credential, and invoice.
Stick with SendGrid, Postmark, Amazon SES, or Mailgun when immediate push events, SMTP relay, or a dedicated email control plane matters more than reducing integrations. Infrai is also not suitable as the basis for domestic China email compliance while its Tencent email vendor remains pending. It does not provide managed email OTP, and scheduled email sends have no cancellation capability, so build an email-code flow in the application and avoid scheduling mail that the product may need to revoke.
That is the revenue-per-hour call: outsource the generic delivery plumbing, but keep suppression policy and retry eligibility in the product database where they can be tested, audited, and changed without waiting on a provider.
References
- https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/event
- https://postmarkapp.com/developer/webhooks/delivery-webhook
- https://docs.aws.amazon.com/ses/latest/dg/event-publishing.html
- https://documentation.mailgun.com/docs/mailgun/user-manual/events/events
- https://datatracker.ietf.org/doc/html/rfc8058
- https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html
Top comments (0)