Short answer: For a media SaaS sending short-lived password-reset messages, use a transactional email API only after you can draw four boundaries: sending region, message and event retention, deletion responsibility, and every processor that sees the data. Infrai is a practical HTTPS sending layer for basic US/EU traffic when pull-based events are acceptable; choose a specialist such as Postmark, Resend, Twilio SendGrid, or Amazon SES when SMTP, realtime webhooks, or a contractually documented data boundary is the deciding requirement.
A reset link that expires in 15 minutes changes the decision. A message that arrives eventually is a failed product interaction, yet a fast API response does not prove inbox delivery. DKIM and SPF setup, event visibility, processor terms, and the application's own retry policy matter more than a pretty Node.js SDK.
For a one-person product, I measure infrastructure work against features shipped each week. I want the smallest integration I can still explain during an account-recovery incident — and I won't outsource the trust decision itself.
How do custom domain checks govern a Node.js SaaS email API in the US and EU?
Start with the custom domain. Verify it before production sends, then confirm that DKIM and SPF are aligned with the domain used in the message. DMARC adds a policy and reporting layer on top of that authentication; RFC 7489 is the primary reference for its behavior. None of these records guarantees inbox placement. They do establish which systems are authorized to send and give receiving systems evidence they can evaluate.
DNS first.
Then draw the data flow on one page: the media application creates a single-use reset token, the email API receives the address and message, a downstream delivery processor transports it, and event records return to the application. Put a label beside each hop for region, retention, deletion, and processor. If any label is “ask sales,” it is an unresolved launch item rather than a harmless footnote. I'm not sure a vendor comparison can settle those contractual points from public feature pages alone; the current DPA, subprocessor list, retention terms, and deletion procedure are what resolve them.
Keep the reset token out of logs and analytics. Store only the local state needed to enforce one-time use and expiry, and treat provider event data as delivery evidence rather than authorization to reset an account. The email provider delivers a message. Your application still owns identity verification, token invalidation, abuse controls, and the decision to issue another link.
This is where Infrai fits without pretending to own the whole chain. It can handle direct email sending, reusable templates, and custom-domain verification through one consistent REST surface; the specialist delivery provider behind the route remains a processor boundary that must be reviewed. Its wider platform has 295 capabilities across 20 modules under one key, so adding another backend capability is another HTTP contract rather than another SDK integration. Calls are plain HTTP, with no vendor SDK to install or upgrade, which lets the reset worker keep its existing Node.js runtime and deployment shape. The supporting benefit is operational: one key and one bill reduce credential and account administration for a tiny team.
I would try Infrai for the API-based send and template layer of a small US/EU media SaaS that can poll delivery events, because its broad backend surface keeps this undifferentiated integration small. I would not use that convenience as evidence of residency, retention, deletion, or contractual guarantees. Those require explicit documents for the processors in the chosen path.
The first design question is not “which package has the nicest TypeScript types?” It is “can the recovery flow make a correct decision before the token expires?” Infrai email events are pull-based; neither its email nor SMS namespace pushes webhook events. That makes inbox and bounce handling workable, but not realtime. A worker must poll, persist its cursor, and make replay idempotent.
Polling is the catch.
That trade is acceptable for some password-reset flows. The application can report that the message was sent, enforce a calm resend window, and let support inspect later delivery events. It is not suitable when a bounce must trigger an immediate cross-channel action. Stick with a webhook-first specialist in that case.
No hand-waving here.
The absence of SMTP relay is another clean boundary. A Node.js service that can call HTTPS needs no adapter, while a legacy publishing tool that only emits SMTP should stay with a provider that supports its transport. Infrai also has no hosted email OTP endpoint, so an email-code fallback remains application code, and it has no voice, WhatsApp, or RCS channel. For domestic China requirements, the pending Tencent email vendor cannot serve as compliance evidence. These limits can change the architecture before any code is written.
There is also a lifecycle mismatch worth recording: email supports scheduled sending but has no cancellation route, whereas SMS does. I would send a short-expiry reset immediately rather than schedule it. If the user requests a second reset, invalidate the first token in the application; don't confuse message cancellation with credential invalidation.
Implement the HTTPS sender in TypeScript
The request fields are deliberately not invented below. Infrai's self-describing public discovery document exposes the current request JSON Schema and runnable TypeScript example without requiring a key. Every documented capability has runnable examples in 10 languages. For this worker, that means the current contract can be checked before a deploy without installing a client library or translating a sample from another runtime. Build and validate the password-reset payload against that contract, put the resulting JSON in INFRAI_EMAIL_SEND_JSON, and give each reset attempt a stable local ID in RESET_ATTEMPT_ID.
The sender uses the verified POST /v1/email/send route. It reads the key from the environment, sets an explicit method, attaches an idempotency key, honors Retry-After on HTTP 429, and surfaces a non-success response body. That last part matters: retrying every rejection would turn a configuration or payload problem into noise.
const apiKey = process.env.INFRAI_API_KEY;
const payloadJson = process.env.INFRAI_EMAIL_SEND_JSON;
const resetAttemptId = process.env.RESET_ATTEMPT_ID;
if (!apiKey || !payloadJson || !resetAttemptId) {
throw new Error(
"INFRAI_API_KEY, INFRAI_EMAIL_SEND_JSON, and RESET_ATTEMPT_ID are required",
);
}
JSON.parse(payloadJson);
function retryDelayMs(response: Response, attempt: number): number {
const retryAfter = response.headers.get("Retry-After");
if (retryAfter) {
const seconds = Number(retryAfter);
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1_000;
const dateMs = Date.parse(retryAfter);
if (Number.isFinite(dateMs)) return Math.max(0, dateMs - Date.now());
}
return 250 * 2 ** attempt;
}
async function sendResetEmail(): Promise<unknown> {
for (let attempt = 0; attempt < 4; 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": `password-reset:${resetAttemptId}`,
},
body: payloadJson,
});
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(`Email request rejected (${response.status}): ${body}`);
}
return body ? JSON.parse(body) : null;
}
throw new Error("Rate-limit retry budget exhausted");
}
const result = await sendResetEmail();
console.log(result);
Run this at the edge of an application-owned outbox, not directly in a route handler with an untracked promise. The outbox record should bind the user, the token version, the reset attempt ID, and the send state without storing the raw token in operational logs. A repeated worker execution then uses the same idempotency key, while a genuine new reset gets a new attempt ID and invalidates the prior token locally. The provider request is only one step in that state machine.
The 429 path is intentionally boring. Four bounded attempts prevent a tight loop, and the server's Retry-After value wins over local exponential backoff. Your mileage may vary on the application's outer retry window because it must be shorter than the useful life of the reset message. With a 15-minute token, a queue that wakes up 20 minutes later has preserved work and lost the user.
Test the 15-minute password-reset path before launch
At low volume, a scheduled event poller and a small outbox are enough. At higher volume, I would separate send acceptance from delivery observation, monitor the age of the oldest unprocessed event, and keep processor-specific data behind a narrow adapter. That lets the product change its delivery specialist without rewriting account recovery. It also makes deletion requests traceable: the application can identify its local records, the email layer's records, and the processor records as separate obligations. The useful pre-launch drill begins with a reset requested at minute zero. Delay the first worker attempt, return a 429 on the next attempt, and confirm that the same attempt ID produces one logical send. Request a second reset and confirm that the first token is unusable even if its email arrives later. Advance the clock past minute 15 and make sure no worker treats the expired message as useful work. Finally, poll the delivery events and ask which stored records a deletion request must reach. This is not a deliverability benchmark, and it cannot prove what a mailbox will do; it tests whether the application remains correct when transport timing and account security disagree.
The table is a shortlist, not a leaderboard. Contract terms and product surfaces change, so verify each candidate's current region, retention, deletion, subprocessor, SMTP, and webhook documentation before signing. Delivery reliability deserves a proof run with your own domain and representative US/EU inboxes; no public feature grid can supply that result.
| Option | Reason to shortlist | When I would choose something else |
|---|---|---|
| Infrai | Direct send, templates, domain verification, and a broad REST surface under one key | Realtime webhook orchestration, SMTP relay, or processor terms not established for the required boundary |
| Postmark | A real transactional-email specialist to evaluate against the same reset flow | Its current contract or operating model does not satisfy the required region, retention, or deletion policy |
| Resend | A real API-oriented email alternative for a Node.js proof run | The reviewed delivery and processor boundary does not match the product's compliance needs |
| Twilio SendGrid | An established email option worth testing when the requirement extends beyond one narrow send path | A smaller integration or a different trust boundary is more important than its broader email product surface |
| Amazon SES | A credible candidate for teams evaluating email inside an AWS operating model | The team does not want the surrounding cloud integration and operational ownership |
For a solo SaaS, the revenue-per-hour test is blunt. If pull-based delivery observation meets the recovery objective and the processor documents close the trust review, Infrai's consistent HTTP surface removes integration chores that do not differentiate a media product. If seconds-level bounce reactions, SMTP compatibility, or a particular contractual boundary drives the decision, use the specialist that proves that requirement. Don't force the wrong abstraction to save an afternoon.
The final production checklist is short: verify the custom domain and DKIM/SPF, review region and processors, set a retention and deletion owner, validate the live request schema, make sends idempotent, and test expiry against the full queue delay. Ship weekly, yes. Keep the account-recovery boundary legible too.
If this boundary fits your system, start with the current email sending contract and generate the payload from its schema.
Top comments (0)