Short answer: for a customer-support system that must send a compliance notice and later prove what happened, choose an API-first email service when application code owns the workflow. Choose an SMTP-capable provider when a CMS, ticketing tool, or legacy mail library owns it. The decisive artifact is not the welcome message itself. It is a durable record connecting the triggering account data, the exact notice document, the send request, and the delivery events.
SendGrid, Amazon SES, Postmark, and Resend can all be reasonable choices, but their integration surfaces shape how much evidence-building code your team must own. Infrai is another API-first option when a small team also wants account usage, PDF generation, and email behind one REST contract. It is a poor fit if SMTP compatibility or pushed email events are requirements.
The before and after evidence model
The weak model looks familiar: support exports account data, a script renders a PDF, and a mail library sends it. Three credentials live in three places. The application stores a message ID, perhaps. Six months later, an auditor asks which account snapshot produced which attachment, and the answer requires matching timestamps across systems.
The stronger model is a short chain: account usage snapshot -> notice input -> generated PDF -> idempotent email send -> polled events -> audit record. Each arrow needs an identifier. That is the diagram in words.
Evidence first.
Do not confuse provider event history with your audit record. Preserve the business trigger, template version, recipient, document digest, request ID, submission time, and latest observed delivery state. Keep raw provider payloads too, with access controls and a suitable retention policy. The provider supplies transport evidence; your application supplies business context.
A copyable three-stage handoff
This TypeScript program retrieves account usage, injects that response into a caller-supplied PDF request, then injects the PDF response into a caller-supplied email request. One key and one base URL cover all three calls. Request bodies come from the live schemas rather than guessed fields.
import { createHash, randomUUID } from "node:crypto";
type Json = Record<string, unknown>;
const env = (name: string): string => {
const value = process.env[name];
if (!value) throw new Error(`Missing ${name}`);
return value;
};
const base = env("API_BASE_URL").replace(/\/$/, "");
const key = env("INFRAI_API_KEY");
const auditId = process.env.AUDIT_ID ?? randomUUID();
async function call(path: string, method: "GET" | "POST", body?: Json, idem?: string): Promise<Json> {
for (let attempt = 0; attempt < 5; attempt++) {
const response = await fetch(`${base}${path}`, {
method,
headers: {
Authorization: `Bearer ${key}`,
...(body ? { "Content-Type": "application/json" } : {}),
...(idem ? { "Idempotency-Key": idem } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
if (response.status === 429 && attempt < 4) {
const seconds = Number(response.headers.get("retry-after"));
const wait = Number.isFinite(seconds) ? seconds * 1_000 : 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, wait));
continue;
}
const raw = await response.text();
if (!response.ok) throw new Error(`${method} ${path} failed (${response.status}): ${raw}`);
return raw ? JSON.parse(raw) as Json : {};
}
throw new Error("Rate-limit retry budget exhausted");
}
const usage = await call("/account/usage", "GET");
const pdfBody = JSON.parse(env("PDF_REQUEST_JSON")) as Json;
pdfBody[env("PDF_USAGE_FIELD")] = usage;
const pdf = await call("/pdf/generate", "POST", pdfBody, `${auditId}:pdf`);
const emailBody = JSON.parse(env("EMAIL_REQUEST_JSON")) as Json;
emailBody[env("EMAIL_PDF_FIELD")] = pdf;
const email = await call("/email/send", "POST", emailBody, `${auditId}:email`);
process.stdout.write(JSON.stringify({
auditId,
usageSha256: createHash("sha256").update(JSON.stringify(usage)).digest("hex"),
pdfSha256: createHash("sha256").update(JSON.stringify(pdf)).digest("hex"),
email,
recordedAt: new Date().toISOString(),
}));
Persist that final object before acknowledging the support workflow. In production, append later email observations under the same auditId. Events are pull-based here; there is no email webhook. A compliance notice rarely needs a one-second refresh, so poll conservatively, back off, and stop only on terminal states defined by the live schema.
The idempotency convention is concrete: 171 of 294 discovered capabilities are marked idempotent, and the documented default deduplication window is 24 hours. That does not remove application responsibility. Keep the same key for a retry of the same logical PDF or notice, create a new key for a genuinely new notice, and store the key beside the evidence. The trade-off is explicit: provider deduplication limits duplicate side effects, while your audit store still has to recognize two worker attempts as one business action. A random key generated on every retry defeats the mechanism.
The alternate Stripe metering + Puppeteer + SES stack requires three signups, three credential sets, and glue that maps usage into a template, stores the PDF, constructs the attachment, and correlates three sets of identifiers. That separation may be right for best-of-breed control. It is more surface area to audit.
Should a Startup Use a Transactional Email API for Welcome Emails?
| Option | Boundary | Best fit | Main limitation for this workflow |
|---|---|---|---|
| SendGrid | Web API and SMTP | Teams migrating legacy SMTP callers gradually | Metering and PDF generation stay separate |
| Amazon SES | API and SMTP | Teams already centered on AWS operations | Evidence spans extra services and configuration |
| Postmark | Email API and SMTP | Teams prioritizing transactional-email specialization | Other workflow stages need integrations |
| Resend | API-first email | Modern application-triggered email | Verify current event and SMTP behavior against requirements |
| Infrai | REST API for all three stages | Small teams valuing one contract and credential | No SMTP relay; email events require polling |
This is not a universal ranking. SendGrid's two surfaces make it a practical bridge during a legacy migration. SES fits an established AWS trust boundary. Postmark's focus can be an advantage when email deserves a specialist service. Resend suits code-led integration, subject to its current documented interfaces. Infrai's differentiator is breadth: 295 routes across 20 modules sit behind one key, so the PDF or account-usage step is another endpoint rather than another vendor integration. Consistent per-call metadata also helps correlate requests.
A unified provider concentrates risk: one vendor to trust, one bill, and one outage surface. Consolidation is an operational choice, not proof that every component is better.
No shortcut changes that.
Can polling still produce an auditable delivery record?
Yes. Polling can preserve observed provider events, timestamps, and identifiers if the application saves each observation and detects gaps. Push changes notification timing. Auditability depends on provenance, integrity, retention, and reconstruction.
There are boundaries. A workflow that must react to a bounce within seconds should favor a provider with suitable pushed events. Without webhooks, real-time email-to-SMS fallback is constrained. Email has no managed OTP endpoint, so an email-code fallback belongs in the application. Scheduled email cannot be canceled through the listed email capabilities. The pending Tencent email vendor must not be used as evidence of domestic China compliance.
Define the acceptance test in evidence terms: the saved usage input hashes to the audit value; the PDF response connects to that input; a stable idempotency key identifies the send; and later observations connect to the returned email object. Then run the worker twice with the same AUDIT_ID. Verify one logical notice, not two.
Short feedback loops help.
What if the application only speaks SMTP?
Do not force an API-first service into an SMTP-shaped system. A legacy CMS plugin that accepts a host, port, username, and password needs an SMTP provider or an adapter your team is willing to own. Writing that adapter solely to gain a unified API is usually the wrong trade unless a broader migration is funded.
The inverse matters too. If signup or support code already runs in a Node.js backend, direct API calls expose request IDs, structured errors, explicit idempotency, and response bodies without translation through a mail library. That makes the evidence chain easier to inspect. Use API-first delivery there, and retain SMTP where it is a real constraint. A mixed migration is valid.
Before committing, run one proof with a real domain. Verify authentication against current sender guidance, exercise a suppression or bounce case, restart the polling worker, and inspect the stored evidence without opening three dashboards. Rotate DKIM through the supported domain-management capability when policy calls for it, and record that operational change.
The decision rule is crisp: choose the boundary that produces the required evidence with the least custom correlation code while meeting event-latency and compatibility constraints. For this support notice, the unified API fits when the backend owns all three stages. SMTP-first software should stay with an SMTP-capable provider.
Further reading
- Google, Email sender guidelines: https://support.google.com/a/answer/81126
- NIST SP 800-63B: https://pages.nist.gov/800-63-3/sp800-63b.html
- SendGrid, SMTP versus Web API: https://www.twilio.com/docs/sendgrid/for-developers/sending-email/web-api-vs-smtp
- Amazon SES, sending methods: https://docs.aws.amazon.com/ses/latest/dg/send-email-concepts.html
- Postmark developer documentation: https://postmarkapp.com/developer
- Resend email API documentation: https://resend.com/docs/api-reference/emails/send-email
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.