← Back to blog

How to Send Transactional Email: API, SMTP & Architecture

· 6 min read

Diagram of an application posting to a send endpoint, branching into API and SMTP delivery, and arriving in a recipient inbox.

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.

The three transports

HTTP APISMTPOwn mail server
SetupAPI keyHost, port, credentialsWeeks
Round tripsOne requestSeveral per messageSeveral per message
Error handlingStructured responseNumeric status codesYours to build
TraceabilityMessage ID returnedRequires log parsingRequires log parsing
Ongoing costProvider feeProvider feeReputation and IP operations
Use whenDefault choiceLegacy or third-party softwareVery high volume only

HTTP API

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.

SMTP

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.

Your own mail server

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.

The architecture that actually matters

Transport is the easy decision. These are the ones that cause incidents.

Send asynchronously

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.

Make it idempotent

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.

Retry with backoff, and know what not to retry

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.

Decide where templates live

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 codeTemplates outside code
Copy changePull request and deployEdit and publish
Who can editEngineers onlyAnyone with permission
Reuse across servicesCopy the fileSame template, called by ID
Version historyMixed into git historyExplicit per template
Integration churnChanges with the contentFixed

Decoupling means your backend sends an identifier and a set of variables; everything else is edited and published separately. The integration stops changing.

Send by API, edit templates without a deploy

Publish a template in SentFast, call it by ID from your backend, and let anyone change the copy.

Get started

Log every send

When 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.

Authenticate before you scale

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.

A checklist before you ship

DecisionDefault answer
TransportHTTP API unless legacy software forces SMTP
DispatchBackground job, never in the request handler
DuplicatesIdempotency key derived from the event
RetriesExponential backoff on 5xx and timeouts only
BouncesHard bounces suppressed permanently and automatically
TemplatesStored outside the application repository
LoggingPer-send record including template version
AuthSPF, DKIM and DMARC configured before scaling
DomainsTransactional subdomain separated from marketing

Related reading