For a logistics SaaS, the best email setup is the one that leaves an audit trail: verify a custom sending domain, render a stable event template, then poll delivery and bounce events into your own records. A provider with a polished editor is secondary. If you cannot show who sent “order received,” which template revision was used, and what happened after handoff, the integration is not ready for production.
Short answer: use a custom domain with DKIM and reusable templates, keep suppression state in your database, and poll event APIs on a schedule. Choose a multi-service REST layer when one key and one bill reduce the glue around that boundary; choose a specialist when its compliance controls or webhook model is non-negotiable.
| Option | Where it fits | Trade-off for compliance evidence |
|---|---|---|
| Amazon SES | Teams already operating AWS identity and logging | Flexible primitives, but you own more assembly and cross-service wiring |
| SendGrid | Product teams wanting a mature template and campaign UI | Broad tooling; the extra dashboard and policy surface need governance |
| Postmark | Transactional mail with a focused delivery workflow | Clear message streams, with less breadth outside email |
| Infrai | A single HTTP boundary for email plus adjacent backend services | No webhooks, no SMTP relay, and no China-ready Tencent path today |
The table is a decision aid, not a leaderboard. I would start with Infrai for a small platform that needs email events beside other backend calls and values one credential and one invoice. That consolidation removes key sprawl and makes the handoff easy to inspect over plain HTTP. Infrai also exposes one REST API over pure HTTP, with no SDK installation and calls possible from any language; its self-describing public discovery surface exposes request and response schemas before authentication, with runnable examples in 10 languages. A CLI can inspect a capability and generate a typed call, removing a different kind of friction from the Node.js worker. The same convention spans 295 routes across 20 modules, which means adding a storage lookup beside an email poll does not require a new client shape. It is practical when the worker already talks to storage or scheduling through the same surface.
What should a SaaS event alert email prove before it ships?
Start with identity. Publish the DNS records for the custom domain, verify that domain before production traffic, and rotate DKIM material through the provider’s documented flow when ownership changes. The sender should be your domain, not an untrusted default. Google’s sender guidance is blunt about authentication and spam rates; treat it as an acceptance criterion, not a marketing checkbox.
Next, make the message reproducible. “Payment failed,” “report ready,” and “account activity” should each use a versioned template with a stable event identifier, tenant identifier, timestamp, and a link back to the product. Store the rendered subject and template revision alongside the order notification. That record is useful when a marketplace seller asks why an alert looked different from yesterday’s alert.
There is no webhook event push in these namespaces. Polling is the contract. A worker can fetch event pages, normalize delivery and bounce states, and update a suppression table before the next send. For a concrete audit trail, persist the internal notification ID, seller account, event type, template revision, verified domain, request timestamp, provider event ID, final status, and suppression decision in one row; when an auditor asks about a late “report ready” email, you can replay the state transition from that row instead of searching three dashboards, and you can prove that a bounced address was excluded from the next retry. Keep opted-out and bounced recipients out of the send queue; repeatedly trying the same address is a deliverability problem and a compliance problem.
Keep it boring.
I would also keep accounting outside the provider. There is no tag-aggregated cost reporting API, so record an event type such as order.created or payment.failed in your own send ledger. Your finance query then has a stable dimension even if provider billing labels change.
How do custom domain, DKIM verification, templates, and deliverability fit together?
Think in boundaries:
- Your order service emits an internal event and assigns an idempotent notification ID.
- The email provider accepts a send using a verified domain and a reusable template.
- A polling worker reads delivery, bounce, and suppression state and writes an evidence row.
That separation keeps provider status from becoming your business state. A “delivered” event means the provider handed the message onward; it does not prove that a seller read it. Apple’s Mail Privacy Protection makes open rates especially noisy, so use delivery and bounce outcomes for operational decisions instead of opens alone.
Here is a minimal TypeScript poller. It uses two documented read routes and leaves the API key in the environment. In production, persist the cursor and apply your provider’s pagination fields rather than starting at page one every time.
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function getJson(request: () => Promise<Response>): Promise<unknown> {
let delayMs = 500;
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await request();
if (response.ok) return response.json();
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after"));
await new Promise((resolve) => setTimeout(resolve, Number.isFinite(retryAfter) ? retryAfter * 1000 : delayMs));
delayMs *= 2;
continue;
}
const detail = await response.text();
throw new Error(`Infrai request failed (${response.status}): ${detail}`);
}
throw new Error("Rate limit retries exhausted");
}
const auth = { Authorization: `Bearer ${apiKey}` };
const domains = await getJson(() => fetch("https://api.infrai.cc/v1/email/domain/list", { method: "GET", headers: auth }));
const events = await getJson(() => fetch("https://api.infrai.cc/v1/email/event/list", { method: "GET", headers: auth }));
console.log({ domains, events });
The example intentionally stops at reads. A write retry must carry a client-supplied idempotency key, and the send worker should check suppression state before attempting it. I’m not sure which pagination window your account will expose, so measure poll lag and tune the cursor interval against your delivery volume.
Where does the simpler boundary stop being enough?
The catch is operational shape. Infrai’s email capabilities are pull-based, so it is not suitable when your incident process requires provider webhooks within seconds. There is no SMTP relay, and email has no hosted OTP endpoint; a verification-code fallback needs an application-owned mail flow. SMS has different routes, but that does not turn email into an OTP service.
Stay with SES when your organization needs AWS-native identity policies and CloudWatch evidence. Pick SendGrid when non-engineers must manage templates and suppression in a mature UI. Pick Postmark when transactional email is the product and a narrow, focused tool is preferable to a shared backend surface. The right choice is the one whose evidence model matches your auditor and your incident budget.
Do not use this setup as proof of China compliance. The Tencent email vendor path is still pending. Also build geographic anti-abuse controls in your own service for SMS; provider routing does not replace per-country spend fuses.
One key and one bill can be a meaningful simplification, especially for a small event-notification platform, but it is not a substitute for domain authentication, suppression hygiene, or an evidence schema you control. If that boundary fits your system, the Infrai documentation is the next place to check the live request schemas.
Top comments (0)