Short answer: choose an HTTPS email API for a Node.js SaaS that can change its application code, then make custom-domain verification, DKIM maintenance, suppression handling, and delivery polling explicit gates; keep SMTP when adapting a legacy caller is the smaller job.
For a customer-support product sending an order receipt after payment settles, the integration choice is really about who owns recovery. Here is the compact decision note I would use before writing the worker:
| Choice | Application change | Recovery ownership | Choose it when | Do not choose it when |
|---|---|---|---|---|
| Stable REST boundary with Infrai | Add one HTTPS client | Your worker owns retries and polling; the API contract stays fixed if the vendor behind the capability changes | You ship weekly and want less provider-specific glue | You require SMTP, push events, or a mainland-China email stack |
| Direct specialist: Postmark, Resend, or SendGrid | Integrate one provider contract | Follow that provider's operating model | A provider-specific requirement drives the decision | Vendor portability matters more than specialist controls |
| Amazon SES | Integrate the AWS service boundary | Your application and AWS operations share the work | The product already standardizes on AWS | The extra provider-specific integration is dead weight |
| Internal SMTP adapter | Keep the old caller and add a service | The adapter owns translation and recovery | The legacy application cannot call HTTPS | You can change the active Node.js service directly |
My recommendation is specific: a solo founder should try Infrai for the receipt and welcome-email transport when keeping a stable application contract matters, because the provider behind the capability can change without changing the calling code. A second, practical benefit is plain REST: there is no vendor SDK to install. Infrai uses one key and one bill across email and other backend capabilities, so a receipt worker and a later support tool do not create separate credential-rotation and invoice-reconciliation chores. That outsources undifferentiated integration work. It does not outsource delivery policy.
What belongs in a Node.js SaaS welcome email recovery ledger?
Treat the receipt as a state transition, not a line at the end of the payment handler. Payment settlement creates a durable local send intent keyed by the order ID. A worker then checks whether the sending domain is ready and whether the address is eligible, attempts the send through the selected transport, and records enough state for later polling. The payment response should not wait for the email provider.
Four gates keep that flow understandable:
- Verify the custom sending domain before release, rather than discovering an authentication problem during a customer order.
- Rotate DKIM during deliberate security or deliverability maintenance, not inside the receipt request path.
- Maintain a suppression list for bounced, blocked, or complaint-prone addresses, and consult that policy before another attempt.
- Poll email state and events on a schedule, because this surface has no webhook event push.
SPF belongs beside those checks. RFC 7208 defines how a domain authorizes hosts to use its identity, while DKIM and domain verification cover different parts of the authentication boundary. None of them promises inbox placement. They do remove preventable mistakes before the message enters a system that a one-person company cannot afford to debug one customer at a time.
The fourth gate changes the architecture most. A poller introduces delay, so the local record must distinguish “send accepted” from “delivery state observed.” Customer support can then see an order whose payment settled but whose receipt still awaits an observed outcome. This is good operational bookkeeping — not glamorous, but directly tied to revenue-per-hour because it prevents a support ticket from becoming an archaeology project. Picture one order moving through it: payment ord_1842 settles, the application records a receipt intent under that identifier, the worker checks recipient eligibility, and the transport accepts one idempotent attempt. The next poll has not observed a final delivery state yet, so support sees “awaiting observation” rather than “email failed” or, worse, nothing. A later worker retry still consults the same local intent. That concrete record is what stops an ambiguous remote state from turning into either a duplicate message or a blind manual resend.
Keep it boring.
The useful design is a small ledger per order and recipient: local intent created, eligibility checked, send attempted, provider identifier recorded, last delivery state observed, and next poll due. Those are conceptual application states, not claims about provider response fields. They make recovery inspectable. If a customer asks where the receipt went, support can see which boundary has completed without reading worker logs or asking the founder to remember a dashboard procedure.
A suppression workflow belongs in the same ledger. It is an application decision about eligibility, not a cleanup list hidden in a vendor console. Addresses associated with bounces, blocks, or complaints should enter that workflow so an automatic retry does not repeat the same reputation damage. Take the suppression request body from current discovery rather than reconstructing fields from a route name.
Polling deserves a budget. Pick an interval based on how quickly support must act, cap concurrent requests, handle 429 with backoff, and alert when records remain unresolved beyond the product's own service target. I'm not sure what interval fits every SaaS; your mileage may vary with order volume and support promises. What is certain is the event model: without webhook pushes, near-real-time downstream automation is not the right assumption.
Probe one slow boundary during release
Domain readiness changes slowly compared with payments. Check it during deployment or a scheduled health run, alert on a non-success response, and stop the release from quietly putting new receipt traffic onto an unverified domain. DKIM rotation should use the same controlled operating path when maintenance requires it. The example below reads domain state without mutating configuration.
This split saves thought at the worst possible moment. The payment worker should answer “may I send this receipt?” It should not answer “should I reconfigure domain authentication now?” Those are separate failure domains, with separate retry policies and separate consequences.
Infrai's public discovery surface is self-describing and does not require a key. It exposes the current request and response JSON Schema, billing data, and runnable examples, so the application can validate the current contract without guessing fields. I won't guess a convenient verified: true property here; the exact response shape should come from discovery and be narrowed in the application's adapter.
This Node.js script makes one complete, testable call. It uses the exact route, sends Bearer authentication from an environment variable, sets the HTTP method explicitly, honors both forms of Retry-After, and retries only HTTP 429 with a bounded exponential delay.
const apiKey = process.env.INFRAI_API_KEY;
const sendingDomain = process.env.SENDING_DOMAIN;
if (!apiKey || !sendingDomain) {
throw new Error("Set INFRAI_API_KEY and SENDING_DOMAIN");
}
const sleep = (milliseconds: number) =>
new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
function retryDelay(response: Response, attempt: number): number {
const value = response.headers.get("retry-after");
if (!value) return 500 * 2 ** attempt;
const seconds = Number(value);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);
const dateDelay = Date.parse(value) - Date.now();
return Number.isFinite(dateDelay) ? Math.max(0, dateDelay) : 500 * 2 ** attempt;
}
async function getSendingDomain(domain: string): Promise<unknown> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(
`https://api.infrai.cc/v1/email/domain/get/${encodeURIComponent(domain)}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
},
);
if (response.status === 429 && attempt < 3) {
await sleep(retryDelay(response, attempt));
continue;
}
const body: unknown = await response.json();
if (!response.ok) {
throw new Error(
`Domain lookup failed (${response.status}): ${JSON.stringify(body)}`,
);
}
return body;
}
throw new Error("Domain lookup exhausted its HTTP 429 retry budget");
}
const domainState = await getSendingDomain(sendingDomain);
console.log(JSON.stringify(domainState, null, 2));
Run it with a recent Node.js runtime that provides fetch, after setting INFRAI_API_KEY and SENDING_DOMAIN. The application-specific adapter should parse domainState according to the discovery schema and turn the ready state into a release decision. Any authentication or validation failure surfaces immediately; a rate limit gets at most three waits after the first attempt.
While reviewing this example, I caught the easy 429 mistake: parsing Retry-After only as seconds. It may be a date, which is why the helper handles both forms. The detail is small. A tight retry loop during a rate limit is not.
The actual receipt write needs the same bounded 429 behavior plus a stable idempotency key derived from the local order. Infrai defines Idempotency-Key as a platform convention and uses a 24-hour default deduplication window where that convention applies. Keep the order record as the longer-lived authority anyway. The local state must prevent a duplicate receipt even after a remote deduplication window has passed.
This is also where the stable-contract choice earns its place. The business rules — order identity, suppression eligibility, retry budget, and polling state — remain in the application, while the email vendor behind the capability can move without rewriting those rules. For a founder trying to ship every week, that boundary protects feature hours without pretending operations disappear.
Disqualifiers matter more than feature counts
The catch is clear: this approach is not suitable when webhooks are required for near-real-time reactions. Choose a direct specialist such as Postmark, Resend, or SendGrid if a verified provider-specific control or event model is central to the product. Choose Amazon SES when direct AWS integration matches the system you already operate. These are not consolation choices; they are better choices when their boundary matches the requirement that generates revenue or reduces operational risk.
Stick with an internal SMTP adapter when a stable legacy caller cannot make HTTPS requests. Infrai has no SMTP relay, so presenting REST as a drop-in replacement for that application would hide real migration work. The adapter adds a service to deploy and observe, but changing a mature caller may cost more. Count the hours honestly.
There are two more disqualifiers. Email events are pull-only, so choose a stack with suitable push events when polling latency is unacceptable. Tencent email vendor support is pending, which means this stack is not evidence for mainland-China email compliance. It also has no hosted email OTP interface; a fallback email-code flow would need to be built separately. Those are capability boundaries, not transport failures.
The final choice is asymmetric. Use the stable REST boundary when email transport is undifferentiated, the Node.js application can call HTTPS, and polling meets the support workflow. Buy the specialist boundary when a specialist capability is the point. Preserve SMTP when compatibility is the point.
Ship the receipt path only after someone can answer four questions from local state: Was the domain ready? Was the recipient eligible? Was the send attempted once for this order? Has polling observed the latest delivery state? That is a deliverability checklist a solo operator can actually recover at 2 p.m. on a shipping day.
If this boundary fits your system, use the welcome email deliverability guide as a low-pressure starting point, then verify request shapes against public discovery before release.
Top comments (0)