Developer Platform
Docs chevron_right Developer Platform chevron_right Transactional email API

Transactional email API

Send receipts, password resets, and notifications on an isolated transactional stream — no campaign, no list membership required. One POST queues a message; delivery, opens, clicks, and bounces are tracked on the message itself, separate from your marketing stats. Requires the sends scope.

How the stream works

Transactional messages live in their own log with a simple lifecycle: queued → sent → delivered, with bounced, complained, and failed as terminal problem states. Three properties define the stream:

  • Queue-and-return. POST /transactional/send validates, snapshots your HTML (or the referenced template's HTML) onto the message row, and returns 201 immediately with status: "queued". The actual vendor send happens in a background job — a template deleted a second after you queue cannot break the send.
  • Isolated engagement. Opens and clicks are counted on the message row (visible in the API detail and the in-app log), never mixed into campaign or automation reports.
  • Isolated policy. Recipients with a hard-bounce or spam-complaint history are refused synchronously; marketing unsubscribes do not block transactional mail by default. The exact rules are on the transactional suppression policy page.

Endpoints

MethodEndpointDescription
POST/transactional/sendQueue one message: to, subject, from_email (required), exactly one of template_uid or html, plus optional merge_data (≤16 KB JSON), from_name, reply_to, track (default true), include_unsubscribe (default false). Rate class api.sends.
GET/transactionalPaginated log, newest first. Filters: status, to, date_from/date_to (Y-m-d), per_page (≤100).
GET/transactional/{uid}One message with merge_data and the full events timeline (queued, sent, delivered, opened, clicked, bounce/complaint entries; the newest 50 are kept).

Sending a message

# Inline HTML with merge_data (fallbacks after | render when a key is absent)
curl -X POST https://emailflow.ai/api/v1/transactional/send \
  -H "Authorization: Bearer efa_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: receipt-ord_42" \
  -d '{
    "to": "alice@example.com",
    "subject": "Your receipt for order {{ order_id }}",
    "html": "

Hi {{ name | there }}, thanks for order {{ order_id }}.

", "merge_data": { "name": "Alice", "order_id": "ord_42" }, "from_email": "billing@yourdomain.com", "from_name": "Acme Billing", "reply_to": "support@yourdomain.com" }' # Response (201) { "data": { "message_uid": "64a91b2c7de31", "status": "queued" } }

Content. Provide either inline html (≤500 KB) or a template_uid from your template library — never both. Merge tags use the same fallback grammar as campaigns: {{ key }} renders from merge_data, {{ key | fallback }} renders the fallback when the key is missing.

From address. from_email must be one of your verified senders or any address at a verified sending domain — unverified addresses are refused with a 422 at queue time rather than a vendor rejection at send time.

Retry safety. Always pass an Idempotency-Key: a network-level retry then replays the original response instead of queuing a duplicate email.

Synchronous refusals

Everything that can be checked up front is refused at queue time with the standard error envelope, so your application learns immediately:

StatuscodeWhen
409CONFLICTThe recipient is suppressed. details.reason is "suppressed" and details.suppression carries the matrix token (blacklisted, spam-reported, transactional-feedback, or unsubscribed) — see the policy page.
402CREDITS_EXHAUSTEDYour monthly send credits are used up (transactional draws from the same wallet as marketing; the usage endpoint reports the per-stream split).
422VALIDATION_ERRORInvalid body — including from_email outside your verified identities, both/neither of template_uid/html, or merge_data over 16 KB.
404NOT_FOUNDtemplate_uid that does not exist in your account.

Tracking, headers & the footer

  • Open/click tracking is on by default (track: true): links are rewritten through your tracking host and an invisible open pixel is appended. Set track: false for a byte-exact body — no pixel, no rewritten links.
  • No unsubscribe footer or List-Unsubscribe header by default — a password reset is not a mailing-list message. Set include_unsubscribe: true to add one: recipients who are contacts in your account get the full RFC 8058 one-click pair (the native Gmail/Apple unsubscribe button); unknown addresses get a mailto: header pointing at your reply-to.
  • No promotional watermark, on any plan — the Free-plan footer that marketing sends carry is never injected into transactional mail.

Delivery feedback & webhooks

Transactional sends go out through a dedicated Amazon SES configuration set, so deliveries, bounces, and complaints route back to the exact message row — the log updates itself within seconds of the mailbox provider's verdict. Each transition appends a timeline event and emits the corresponding webhook (email.delivered, email.bounced, email.complained) with stream: "transactional" and message_uid in the payload, so one receiver can tell the two streams apart. Messages are retained for 13 months, then purged automatically.

emailflow.ai/rui/sending/transactional
The Transactional page under Sending: four 30-day stat cards (sent, delivered, opens, issues), the honor-marketing-unsubscribes policy toggle, and the searchable message log with status chips per message
Every API send lands in the in-app log — Sending → Transactional — with 30-day stats and an expandable per-message events timeline.

Using the SDK

The TypeScript SDK wraps the stream as emailflow.transactional, with the same idempotent-retry semantics:

const { message_uid } = await emailflow.transactional.send(
  {
    to: 'alice@example.com',
    subject: 'Reset your password',
    html: resetEmailHtml,
    from_email: 'security@yourdomain.com',
    track: false,
  },
  { idempotencyKey: `pw-reset-${userId}-${tokenId}` },
);

const message = await emailflow.transactional.retrieve(message_uid);
console.log(message.status, message.events);

A full receipt-sending recipe (send, poll the outcome, react to a suppression refusal) ships in the SDK cookbook.