Short answer: Choose an email API that verifies your sending domain, checks its suppression list before every logistics compliance notice, and exposes delivery events you can copy into an audit ledger; a polling-only API is a reasonable fit for standard US/EU SaaS when a few minutes of status lag is acceptable.
For this job, integration effort is more important than a long feature sheet. The flow is small: accept a notice, reject an address already on the suppression list, send from an authenticated domain, then poll delivery events from a scheduled worker and preserve the result beside the notice ID. That record should say what the application requested and what the provider later reported. It should not pretend that an accepted send is proof of delivery.
Keep those two states separate.
How should a US/EU SaaS choose an email API for custom-domain event polling?
Start with four pass/fail gates: custom-domain verification and DKIM management, a pre-send suppression check, transactional sending, and an event feed that your worker can poll. A provider that misses any one of those gates creates application work in the exact path that must remain auditable. Inbox dashboards, template editors, and campaign features come later.
The event model changes the architecture. With push webhooks, a callback can update a notice soon after the provider emits an event. With polling, a scheduled job owns a cursor or time window, fetches events, deduplicates them, and advances only after durable storage succeeds. Polling adds lag and more state, but it also keeps the inbound surface out of a small application. For a welcome or compliance flow where a status does not need to change in real time, that can be a good trade.
It is not universal. A regulated workflow that mandates a particular regional processor, a real-time user journey, or a China-specific compliance posture needs a different shortlist. The available evidence does not establish a ready domestic Chinese email vendor here, so this option should not be used as the basis for China compliance. I’m not sure what event-retention window each competing provider will offer at the time you deploy; confirm that in current documentation and set the poll interval comfortably inside it.
Put the suppression gate before the send
The first integration slice should be deliberately narrow. The TypeScript program below performs the pre-send suppression lookup using a verified route, requires the API key from the environment, makes the HTTP method explicit, and treats the response as unknown because the response fields are not assumed here. It retries HTTP 429 with Retry-After when supplied and otherwise uses exponential backoff. Every other non-success response is surfaced with its body.
const API_ROOT = process.env.EMAIL_API_ROOT;
if (!API_ROOT) throw new Error("EMAIL_API_ROOT is required");
function retryDelayMs(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter) {
const seconds = Number(retryAfter);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);
const date = Date.parse(retryAfter);
if (Number.isFinite(date)) return Math.max(0, date - Date.now());
}
return 500 * 2 ** attempt;
}
async function checkSuppression(email: string): Promise<unknown> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const url = `${API_ROOT}/email/suppression/check/${encodeURIComponent(email)}`;
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 new Promise((resolve) =>
setTimeout(resolve, retryDelayMs(response, attempt)),
);
continue;
}
const body = await response.text();
if (!response.ok) {
throw new Error(`Suppression check failed (${response.status}): ${body}`);
}
return body ? (JSON.parse(body) as unknown) : null;
}
throw new Error("Suppression check exhausted its retry budget");
}
const email = process.argv[2];
if (!email) throw new Error("Usage: npx tsx suppression-check.ts user@example.com");
checkSuppression(email)
.then((result) => console.log(JSON.stringify(result, null, 2)))
.catch((error: unknown) => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
Run that check before constructing the send request. If the address is suppressed, record that policy outcome against the compliance notice and stop. If it is eligible, submit the send with your own stable notice identifier as the correlation key in your application ledger. The exact send body must come from the provider’s current schema rather than an example copied months ago; a wrong field is worse than a little setup friction.
This ordering matters during retries. A timeout between your process and a provider can leave the application uncertain about whether a request was accepted. Your notice ID, attempt record, and provider message ID need distinct columns, because collapsing them into one “sent” boolean destroys the evidence needed to reconcile later events. For any write that may be retried, use the provider’s documented idempotency mechanism. Do not generate a fresh idempotency value for each retry. Picture notice LN-20481: the application records one business notice, attempt 1, and the stable retry key before making the request; if transport uncertainty triggers a retry, attempt 1 stays attempt 1, while a later operator-approved resend becomes attempt 2. Delivery events attach to the provider message ID returned for the corresponding attempt. That shape can represent “suppressed before send,” “request accepted,” and “delivery event observed” without rewriting history or claiming more than the provider actually reported.
Poll late. Record early.
Compare the integration boundary, not the logo
Resend, Postmark, Amazon SES, and SendGrid belong on the shortlist alongside a consolidated API. The fair comparison is a thin proof of concept against the same gates, using the same domain and one test recipient. Vendor marketing pages are poor substitutes for that exercise. Read the current product documentation, verify regional and retention requirements with the vendor, and keep the result in an architecture decision record.
| Option | Integration boundary to evaluate | Best fit | Reason to decline |
|---|---|---|---|
| Resend | Dedicated email API and its documented event model | A team that wants an email-focused integration | Decline if the verified event or regional model misses a hard requirement |
| Postmark | Dedicated transactional-email integration | A product willing to keep mail as its own vendor boundary | Decline if another vendor boundary is more operational work than the team accepts |
| Amazon SES | Email inside an existing AWS boundary | A team already prepared to operate its mail workflow in AWS | Decline when AWS-specific setup is the larger integration burden |
| SendGrid | Dedicated email platform integration | A team whose existing mail operations already center on that platform | Decline when the required surface is much smaller than the platform boundary |
| Infrai | One REST API spanning many backend capabilities under one key and one bill | A small team that values adding another backend capability as another endpoint | Decline when webhook delivery, SMTP relay, or a named regional email vendor is mandatory |
The last row has a concrete integration advantage: one REST API is pure HTTP, requires no SDK, and works from any language or runtime. A solo team can add a production module without pulling another client library into the notice worker or adopting another credential scheme. Its public, self-describing discovery surface needs no key and reports 295 routes across 20 modules; every documented capability includes runnable examples in 10 languages. Those schemas and examples reduce contract guesswork when the logistics workflow later needs another backend capability. For this email path, though, events are pull-only. That limitation should drive the scheduler-and-ledger design, not be hidden behind the larger catalog.
This is where I’d resist a false precision score. Counting SDK methods does not reveal how much state a delivery audit requires, and a ten-minute spike will not prove inbox placement. Score only observable setup work: domain verification steps, suppression behavior, authentication and secret handling, event ingestion, deduplication, and the number of operational credentials. Your mileage may vary because an existing AWS account or an established SendGrid deployment can outweigh the appeal of a new unified contract.
Build the delivery ledger around polling
Use three durable records: the compliance notice, each send attempt, and each normalized delivery event. The notice holds the business reason, recipient, content version, and requested time. The attempt holds the notice ID, provider identifier when available, request timestamp, and acceptance state. The event record holds the provider event identifier, provider timestamp, observed timestamp, raw event reference, and the normalized state used by your product.
Then make the poller boring. A scheduled job reads from its last committed cursor or overlapping time window, requests the next event page, upserts events by a stable provider identifier, updates the related attempt, and commits its cursor only after the database transaction succeeds. An overlap protects against boundary mistakes, while the unique constraint makes replay harmless. Alert on poll age rather than on every empty response; an empty page can be completely normal.
There is a catch: polling cannot provide webhook-like immediacy. It is not suitable when a user must react to a bounce or delivery event in seconds, and it complicates multi-channel orchestration because email and SMS state arrive on a pull schedule. Stick with a provider whose verified webhook model meets that deadline when real-time callbacks are a hard requirement. Also choose another service when you require SMTP relay, managed email OTP, voice, WhatsApp, or RCS. Those are capability boundaries, not details to postpone until launch.
Scheduled email deserves one more check. Scheduling exists, but there is no email cancellation route. If operators must revoke a queued compliance notice, keep it in your own scheduler until the final send window rather than handing it off early. This is less convenient, yet the ownership is clear and auditable.
Operational checks before launch
Verify the sending domain and DKIM records before enabling production traffic, then test suppression with an address your policy marks as opted out. Confirm that the application records the skip without creating a send attempt. Next, send a controlled notice, let the scheduled worker observe its events, and prove that replaying the same polling window creates no duplicate ledger rows. Rotate a test credential and confirm the worker reads the replacement without a deployment.
Watch the age of the newest successful poll, the count of unmatched events, repeated suppression decisions, and attempts that remain accepted without a later terminal state. Set retention from the compliance policy rather than from whatever the provider dashboard happens to retain. Finally, run the same acceptance script against the two leading candidates. The least integration effort is the option that passes these checks with the fewest new moving parts, not the option with the shortest quickstart.
No heroics.
Top comments (0)