Short answer: choose the easiest HTTP email API that gives your property onboarding flow durable bounce evidence and deterministic suppression; the lowest advertised price is secondary.
A rental platform sends welcome messages, lease links, and identity checks to applicants and owners in the EU and US. A typo in one address is not merely a delivery metric. It can create repeated contact attempts, muddy consent records, and leave support unable to explain what happened. I design the mail path as an evidence pipeline: request, provider response, event, suppression decision, and an immutable audit record.
What should a Node.js startup verify before choosing an email API for EU and US onboarding?
Start with the event contract, not a feature grid. The API should return a stable message identifier after accepting a request, and its event feed should distinguish a hard bounce, a temporary failure, a complaint, and a successful delivery attempt. Your application owns the recipient state; the provider is a transport and signal source.
For property records, keep the minimum data needed to prove a decision: tenant or owner ID, normalized address hash, message ID, event type, event timestamp, and policy version. Do not put lease content or identity documents into a webhook payload. SPF tells receiving systems which senders are authorized, while identity guidance from NIST places the responsibility for clear authenticator and recovery flows on the relying application.
The practical acceptance test is small. Send a welcome message to a controlled mailbox, force a syntactically invalid address in a staging tenant, replay the same event twice, and deliver events out of order. A service that passes only the happy path is not easy; it is just quiet.
Keep it boring.
A minimal Node.js evidence path
This example uses fetch, so it needs no SMTP connection or vendor SDK. The endpoint is deliberately abstract; map it to the chosen provider's documented route and preserve the response ID.
type MailAccepted = { id: string };
type DeliveryEvent = {
id: string;
recipient: string;
type: "delivered" | "bounced" | "complained" | "deferred";
occurredAt: string;
};
export async function sendWelcome(
apiBase: string,
apiKey: string,
recipient: string,
propertyId: string,
): Promise<MailAccepted> {
const response = await fetch(`${apiBase}/messages`, {
method: "POST",
headers: {
authorization: `Bearer ${apiKey}`,
"content-type": "application/json",
"idempotency-key": `welcome:${propertyId}:${recipient.toLowerCase()}`,
},
body: JSON.stringify({
to: recipient,
template: "property-welcome",
variables: { propertyId },
}),
});
if (!response.ok) {
throw new Error(`mail submission failed with status ${response.status}`);
}
return (await response.json()) as MailAccepted;
}
export function applyEvent(event: DeliveryEvent, state: { suppressed: boolean }) {
if (event.type === "bounced" || event.type === "complained") {
state.suppressed = true;
}
}
The idempotency key prevents a retry from sending two welcomes. In production, verify webhook signatures, store the raw event for a bounded retention period, and make applyEvent monotonic: a later delivered signal must not unsuppress a hard bounce. Keep suppression writes transactional with the audit row, then expose a support view that answers “why was this recipient skipped?” in one query.
Failure modes that make a cheap service expensive
A 202 response means accepted for processing, not delivered. If you treat it as success, a mailbox rejection can go unnoticed until a tenant reports it. Temporary failures need a retry policy with a ceiling; permanent bounces need suppression before the next scheduled reminder. Webhooks can be duplicated, delayed, or delivered out of order, so event IDs and timestamps belong in a deduplication table. I also retain the provider's reason code alongside my own normalized category: a 550-style permanent rejection should block future onboarding mail, while a timeout or a 4xx deferral should enter a bounded retry queue. That distinction keeps an overnight lease reminder from turning a transient DNS hiccup into a permanent suppression, and it gives support a precise explanation instead of a vague “email failed” label.
DNS alignment is another boundary. Publish SPF for authorized senders, sign with DKIM, and set a DMARC policy that matches the domain you show in the From header. Keep those records in infrastructure code so a domain move does not silently reset your evidence trail.
I keep one deliberately boring dashboard: accepted, delivered, deferred, bounced, complained, and suppressed counts by property and region. It caught a bad import of 37 addresses in a test dataset before any reminder job ran. Your mileage may vary, but a visible counter beats a clever alert that nobody checks.
Cost, geography, and the catch
Compare total operating cost: API calls, webhook storage, log retention, domain work, and the engineer-hours needed to debug missing evidence. Regional processing can matter for contractual reasons, yet an “EU endpoint” alone does not establish data residency or compliance. Ask where message content, logs, and backups are processed, and get the answer in writing.
The catch is that an HTTP API is not suitable when your team requires a fully managed marketing suite, visual campaign editing, or a long-lived SMTP relay for legacy devices. Stick with a provider that supplies those controls when they are the actual constraint. Conversely, a startup that only needs transactional onboarding should avoid paying for unused campaign features and should keep its own transport interface so switching later does not rewrite business logic.
Before launch, require five artifacts: the API schema and message-ID behavior, a signed webhook example for each bounce class, SPF/DKIM/DMARC ownership, a documented retention and residency statement, and a replayable staging test. Review them every 30 days during onboarding changes. If a candidate cannot produce one artifact, record the gap as a risk and choose the option that makes the gap explicit. That is easier to defend than a spreadsheet column labeled “cheap.”
Top comments (0)