· 6 min read

Sending one email is easy. Sending a million without duplicates, without blocking user requests, and without a deploy every time someone fixes a typo is an architecture problem.
This guide covers the transport options, the decisions around them, and the failure modes that only appear in production. For the concept itself, see the transactional email guide.
| HTTP API | SMTP | Own mail server | |
|---|---|---|---|
| Setup | API key | Host, port, credentials | Weeks |
| Round trips | One request | Several per message | Several per message |
| Error handling | Structured response | Numeric status codes | Yours to build |
| Traceability | Message ID returned | Requires log parsing | Requires log parsing |
| Ongoing cost | Provider fee | Provider fee | Reputation and IP operations |
| Use when | Default choice | Legacy or third-party software | Very high volume only |
You send JSON to an endpoint and get a structured response, usually with a message ID you can later correlate with delivery events.
curl -X POST https://sentfa.st/api/v1/email/send \
-H 'Content-Type: application/json' \
-H 'x-api-key: YOUR_API_KEY' \
-d '{
"id": "em_9fd2a1c7b4e30a58",
"to": "user@company.com",
"params": { "user_name": "Laura", "action_url": "https://app.com/reset/abc123" }
}'
One request, one response, and errors arrive as status codes your code already knows how to branch on.
The original protocol, and still the right answer in one situation: integrating software you do not control that only speaks SMTP.
The cost is that SMTP is conversational — a handshake, authentication, envelope, headers, body, termination. Each step is a round trip, which makes it slow over long distances and awkward to debug, because failures surface as numeric codes long after your request completed.
Full control, and a permanent job. You own IP warming, reputation monitoring, blocklist delisting, and keeping authentication correct as standards change. Below very high volume the provider fee is cheaper than the engineering time.
Detailed comparison: Transactional Email API vs SMTP.
Transport is the easy decision. These are the ones that cause incidents.
The tempting implementation is to send inline, in the request handler that created the order.
// Don't do this
await db.orders.create(order)
await email.send(receipt) // couples your response to a third party
return res.json({ ok: true })
Two problems. Your API response time now includes a third-party call. And if the provider is down, the request fails — even though the order succeeded. The user sees an error for something that worked.
Enqueue instead, and let a worker handle delivery and retries.
await db.orders.create(order)
await queue.add('send-receipt', { orderId: order.id })
return res.json({ ok: true })
The trade-off is that you no longer know synchronously whether the email went out, which is why the logging below matters.
Retries are guaranteed — at the network layer, in your queue, in your own error handling. Without protection, each one produces another email.
Derive a stable key from the event, not from the attempt:
const idempotencyKey = `receipt:${order.id}`
Record it before dispatch and check it before sending. A duplicate receipt is confusing; a duplicate payment failure notice generates a support ticket. More on this in email retries and idempotency.
Not all failures are equal. A 4xx for a malformed address will fail identically forever — retrying it wastes quota and hurts your reputation. A 5xx or a timeout is worth retrying with exponential backoff.
Hard bounces must be suppressed immediately and permanently. Repeatedly sending to a dead address is one of the fastest ways to damage a sending domain.
This is the decision teams regret most, because it looks trivial at the start.
The default is to keep email HTML in the application repository. It works until the first copy change, and then every subject line fix needs a pull request, a review, a deploy and a release window — and the person who noticed the problem cannot fix it.
| Templates in code | Templates outside code | |
|---|---|---|
| Copy change | Pull request and deploy | Edit and publish |
| Who can edit | Engineers only | Anyone with permission |
| Reuse across services | Copy the file | Same template, called by ID |
| Version history | Mixed into git history | Explicit per template |
| Integration churn | Changes with the content | Fixed |
Decoupling means your backend sends an identifier and a set of variables; everything else is edited and published separately. The integration stops changing.
Publish a template in SentFast, call it by ID from your backend, and let anyone change the copy.
Get startedWhen a customer says "I never received it", you need an answer in seconds. Record, per dispatch: the recipient, the template version used, the status, the provider message ID and the timestamp.
The template version matters more than it sounds. Without it, you can tell the customer what the template says today, not what they were sent.
SPF, DKIM and DMARC are required by Gmail and Yahoo for bulk senders, and misconfiguration is the most common reason transactional email silently lands in spam. Set them up before volume grows, not after delivery drops — see SPF, DKIM & DMARC.
Send transactional mail from a subdomain separate from marketing so the two reputations cannot contaminate each other.
| Decision | Default answer |
|---|---|
| Transport | HTTP API unless legacy software forces SMTP |
| Dispatch | Background job, never in the request handler |
| Duplicates | Idempotency key derived from the event |
| Retries | Exponential backoff on 5xx and timeouts only |
| Bounces | Hard bounces suppressed permanently and automatically |
| Templates | Stored outside the application repository |
| Logging | Per-send record including template version |
| Auth | SPF, DKIM and DMARC configured before scaling |
| Domains | Transactional subdomain separated from marketing |