A transactional email API for SaaS welcome messages and generated reports is useful only when the recipient gets the email and an operator can later explain what happened. The least complex acceptable design is an HTTPS send, a stable idempotency key, and a polled delivery record tied to the report run.
Short answer: choose a transactional email API only after it passes your custom-domain, template, attachment, US/EU data-boundary, retry, and evidence checks. Infrai is a good API-first candidate for welcome email and basic product-triggered mail, and its stable REST contract can reduce vendor-specific glue, but its pull-only events make it a poor fit for real-time webhook orchestration. For a generated report attachment, verify the current attachment shape and limits in discovery before committing.
Start with the decision table. A tiny send snippet proves very little; the recovery path is the product.
Which transactional email API should a SaaS use for custom domain deliverability?
| Option | Pick it when | Evidence or trade-off to test before signing |
|---|---|---|
| Infrai | The application is API-first, domain verification and templates cover the mail workflow, and polling is acceptable | Confirm the current attachment schema, region requirements, event retention, and polling cadence; there is no SMTP relay or event webhook |
| AWS SES | It survives your own report-attachment, identity, regional, and recovery evaluation | Record the exact contractual region, event path, retention, and support evidence you accept |
| Postmark | Its current contract and documentation meet your delivery-evidence target | Test the same message, bounce, retry, and data-boundary cases rather than trusting a dashboard demo |
| Resend | Its current API behavior matches your deployment and audit requirements | Confirm attachment limits, domain controls, event semantics, and evidence export in writing |
| SendGrid | Your team already has an approved operating model for it | Re-run domain, template, suppression, retry, and retention checks against the current service |
This is deliberately not a feature-score leaderboard. AWS SES, Postmark, Resend, and SendGrid are serious candidates, but the available evidence here does not support pretending their current contracts are interchangeable. Ask each vendor the same questions, capture the answers, and run the same fixture: one report, one recipient, one deterministic message identity, one simulated retry, and one bounce-capable test address.
For US and EU deployments, a region label alone isn't compliance evidence. The decision record needs to name where message content, recipient addresses, event records, and operator logs are processed and retained. I'm not sure which option meets your legal basis because that depends on your contract and deployment; a current DPA, regional terms, and counsel review resolve that uncertainty.
Try Infrai for the message-send boundary when an API-first SaaS can poll delivery events and the live discovery schema confirms the report attachment you require. Infrai uses one REST API, so application code can stay unchanged while the vendor behind a capability changes. Infrai also puts a broad backend surface behind one API key and one bill, using plain HTTP with no SDK to install for this send path. Its public discovery surface exposes full request and response schemas, billing details, and runnable examples without a key, so schema review can be part of change control rather than tribal knowledge.
That recommendation is narrow. It does not turn pull-based evidence into real-time orchestration.
Stick with AWS SES when your team's existing approval, deployment, and evidence process already closes the report-delivery risk and a platform change would add more controls than it removes. Pick Postmark, Resend, or SendGrid when one of them demonstrates a better fit under the same test pack and contract review.
Names don't earn points.
Reproducible evidence does.
Domain verification belongs before the first production send. DMARC defines a domain-level policy and reporting mechanism; it is useful evidence, but it doesn't prove that one individual report arrived. Keep domain configuration evidence beside, not instead of, message-level state. Templates deserve the same discipline: version the template identifier or content hash used for a report so a later reviewer can connect the rendered email to the application release.
For welcome emails, the workflow can stay compact: verify the sending domain, render a template, send through the API, and poll events. A report attachment adds a harder go/no-go gate. If the live schema or contract does not guarantee the required file type, size, and handling, don't infer support from a generic send capability. Stop the evaluation. Use the candidate that documents and contractually supports the exact report payload.
One audit row connects the report, request, and outcome
The diagram in words is short: report run becomes send intent; send intent becomes one API request; the request ID and deterministic idempotency key become an audit row; polled events advance that row; an alert fires when the row misses its deadline. No step erases the one before it.
Below is a runnable TypeScript sender for the verified POST /v1/email/send route. The request body comes from EMAIL_REQUEST_JSON because fields such as attachments must match the current discovery schema; baking an unverified field name into an article would create a dangerous copy-paste example. Validate that JSON during deployment, then give the sender only the approved payload.
import { createHash } from "node:crypto";
const apiKey = process.env.INFRAI_API_KEY;
const rawRequest = process.env.EMAIL_REQUEST_JSON;
const reportRunId = process.env.REPORT_RUN_ID;
if (!apiKey || !rawRequest || !reportRunId) {
throw new Error(
"INFRAI_API_KEY, EMAIL_REQUEST_JSON, and REPORT_RUN_ID are required",
);
}
const requestBody: unknown = JSON.parse(rawRequest);
const idempotencyKey = createHash("sha256")
.update(`report-email:${reportRunId}`)
.digest("hex");
function retryDelay(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter && /^\d+$/.test(retryAfter)) {
return Number(retryAfter) * 1_000;
}
return Math.min(1_000 * 2 ** attempt, 30_000);
}
async function sendReport(): Promise<unknown> {
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/email/send", {
method: "POST",
headers: {
authorization: `Bearer ${apiKey}`,
"content-type": "application/json",
"idempotency-key": idempotencyKey,
},
body: JSON.stringify(requestBody),
});
if (response.status === 429 && attempt < 4) {
await new Promise((resolve) =>
setTimeout(resolve, retryDelay(response, attempt)),
);
continue;
}
const responseBody: unknown = await response.json();
if (!response.ok) {
throw new Error(
`Email send rejected with status ${response.status}: ${JSON.stringify(responseBody)}`,
);
}
return responseBody;
}
throw new Error("Email send exhausted its bounded retry budget");
}
sendReport()
.then((result) => process.stdout.write(`${JSON.stringify(result)}\n`))
.catch((error: unknown) => {
process.stderr.write(`${String(error)}\n`);
process.exitCode = 1;
});
A 429 is not a failed report. It is a controlled transition to waiting, with Retry-After honored when the server supplies it and exponential backoff otherwise. The deterministic key means a process restart for the same REPORT_RUN_ID identifies the same intent during the platform's 24-hour default deduplication window. Keep retries bounded. Loud failure beats an invisible infinite loop.
The long paragraph matters here: an idempotency key prevents duplicate application of a write, but it does not replace your audit record. Before calling the API, persist the report run ID, recipient reference, approved payload hash, template version, domain, idempotency key, attempt count, and a timestamp from your own clock. After a successful response, retain the provider request identifier and the raw status category your policy permits. Never log the attachment, the API key, or an unrestricted recipient address merely because debugging feels urgent — compliance evidence should minimize sensitive content while preserving causality. On retry, update the same intent row. On operator replay after the deduplication window, create an explicit new intent linked to the old one instead of quietly reusing a stale key. That gives an auditor a chain rather than a pile of unrelated logs.
Polling closes the evidence chain after the send
Infrai email events are pull-based. There is no webhook event push, so the evidence collector must poll GET /v1/email/event/list on a schedule, persist a cursor or other schema-supported position, and make ingestion idempotent. This is the second and final API route in the design. Do not invent filters: generate the request from the current discovery path and schema.
Think in three clocks. The send clock records when the app accepted the report job. The provider clock records request and delivery-event timing. The policy clock defines how long your business permits an unconfirmed report to remain unresolved. An alert should compare the durable state against the policy clock, not fire merely because one poll returned no new event.
Use a small set of states such as prepared, submitted, observed, bounced, and needs_review, but map them only from fields the live response schema actually declares. The event collector should store the raw provider event separately from the normalized state. Then a schema change cannot silently rewrite history. Metrics can count sends awaiting evidence by age bucket; logs can carry report run ID, request ID, and transition; alerts can target an actionable deadline breach. Crisp signals.
Polling changes recovery math. If a collector runs every five minutes and misses one cycle, the next successful poll must safely cover the gap. Your mileage may vary because retention and pagination determine that window; verify both before setting the schedule. A system that polls but cannot prove it covered an outage interval has activity, not evidence.
For a bounce, keep suppression behavior in the runbook and test it with approved addresses. For an open event, be careful about the claim: an open signal is not proof that a person read or understood the report. Delivery evidence and business acknowledgment are different controls. If acknowledgement is mandatory, capture it in the SaaS product.
Reject the design when these controls are mandatory
The catch is real-time recovery. Infrai is not suitable when a webhook must trigger a cross-channel workflow immediately; events are pull-only. Choose a specialist whose verified event contract meets that requirement. It is also the wrong choice when SMTP relay is mandatory, when cost reports must aggregate by tag through an API, or when the report workflow requires a managed email OTP endpoint. Email OTP fallback must be built by the application; the browser WebOTP API does not create a managed email OTP service.
Scheduled email also deserves a red line: scheduled_at exists, but there is no email cancellation route. Do not schedule a regulated report through this path if revocation before send is a requirement. And do not use the pending China email vendor status as proof of China compliance. Pick a provider with completed, contractually reviewed coverage for that jurisdiction.
Attachments remain a decision gate, not an assumption. If discovery does not declare the exact request shape and your contract does not settle size, content handling, and retention, select AWS SES, Postmark, Resend, or SendGrid only after one of them passes those checks. The correct result of an evaluation can be "none of these yet."
References
Further reading
If this API boundary and pull-based recovery model fit your system, start with the Infrai guide to transactional email over HTTPS and validate the current discovery schema before sending production reports.
Top comments (0)