RefreshDocsConsole →

API reference

Emails

Send, batch-send, retrieve and cancel messages.

Quick start

Start with a Test key (API keys page, mode Test). Sends with a Test key never leave the platform. The worker simulates the outcome (delivered, or a bounce for bounce@simulator.amazonses.com), writes the same events, and counts nothing against your domain. When the integration works, mint a Live key and change one environment variable.

Use POST /emails/batch for more than one message. One call sends up to 100 messages and returns one id per item. A loop over POST /emails hits the per-key rate limit (429 with Retry-After), and each call counts one request.

SMTP

The SMTP relay is the one ingress that is not HTTP. Each message that it accepts becomes one POST /emails with the same key, limits, checks and log row. The reference page lists the host, the reply codes, the headers that the relay keeps and the ones it replaces.

Send an email

POST /api/v1/emails

Body

Field Type Notes
from string Required. email or Name <email>. The domain must be one of the verified domains of the organization (and allowed for the key).
to string | string[] Required. One address or an array.
cc string | string[] Optional.
bcc string | string[] Optional. to + cc + bcc ≤ 50 addresses.
reply_to string Optional single address.
subject string Required unless template is set. 1–998 chars, no line breaks.
html string Body; ≤ 2 MB. html or text (or both) required unless template is set.
text string Plain-text body; ≤ 2 MB.
template object Send from a published template instead of subject/html/text (mutually exclusive). See below.
headers object Extra headers, { "X-Entity-Ref-ID": "…" }. Names [A-Za-z0-9-], values ≤ 1000 chars, no line breaks. Names the platform sets (Message-ID, Feedback-ID, Return-Path, DKIM-Signature, From, To, Cc, Bcc, Reply-To, Sender, Subject, Date, MIME-Version, Content-Type, Content-Transfer-Encoding, Content-ID, Content-Disposition) answer 422 reserved_header. The platform drops List-Unsubscribe and List-Unsubscribe-Post.
tags array Up to 20 { "name", "value" } pairs (name ≤ 64, value ≤ 256). Returned in webhook payloads.
kind string Message class, ≤ 64 chars, e.g. "invite", "digest.weekly". Bulk prefixes (bulk., broadcast., digest., sequence.) require exactly one recipient and add unsubscribe headers. Also read from a kind tag if the field is absent.
scheduled_at string ISO 8601 with offset (2026-09-01T09:00:00-04:00). Message stays queued until then and can be canceled.
idempotency_key string 1–256 chars. Same effect as the Idempotency-Key header.
track object { "opens": true, "clicks": true }. Default both off. Needs the tracking host of the domain and a single recipient.

template

Field Type Notes
id / slug string One of them required.
version number | "latest" A published version number; omitted or "latest" = highest published. Drafts never resolve.
data object Values for the declared variables of the template (dotted paths become nested objects: { "user": { "name": "Jane" } } for {{user.name}}).

The platform renders the template when it accepts the request, so the stored message is exactly what goes out. A render error returns 422 template_variables. A missing or unpublished template returns 422 template_not_found.

attachments

Up to 10 files per message, 10 MiB in total after decoding. Each item is { "filename", "content", "content_type" }. content is base64. content_type defaults to application/octet-stream. The platform stores attachments with the message body (same retention) and never returns the content through the API — GET /emails/:id lists filename, content_type and size. Batch requests do not accept attachments (422 attachments_not_in_batch).

{ "from": "Acme <billing@example.com>", "to": "jane@example.org", "subject": "Invoice 1234", "text": "Attached.", "attachments": [{ "filename": "invoice-1234.pdf", "content": "JVBERi0xLjQK…", "content_type": "application/pdf" }] }

Response

{ "id": "cmf9x1v0000018lfr6zot86p" }
  • The platform drops recipients on the suppression list before it queues the message. It lists them in "suppressed": ["a@b.c"]. If no recipient remains, it stores the message with status suppressed and does not send it.
  • A replayed idempotency key returns the original id with the header Idempotent-Replayed: true.

Example

curl -X POST https://api.f5send.com/api/v1/emails \
  -H "Authorization: Bearer $F5SEND_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: digest:2026-08-17:jane" \
  -d '{
    "from": "Acme Digest <digest@example.com>",
    "to": "jane@example.org",
    "kind": "digest.weekly",
    "template": { "slug": "weekly-digest", "data": { "user": { "first_name": "Jane" }, "count": 3 } },
    "tags": [{ "name": "week", "value": "2026-33" }],
    "track": { "opens": true, "clicks": true }
  }'

Send a batch

POST /api/v1/emails/batch

The body is a JSON array of 1–100 send objects (same fields as above). The API processes the items in order. A failing item does not stop the rest.

{
  "data": [
    { "id": "cmf9x1v0000018lfr6zot86p" },
    { "error": { "code": "domain_not_verified", "message": "Domain other.com is pending" } }
  ]
}

Per-item idempotency_key works. The API does not apply the Idempotency-Key header to batches.

Retrieve an email

GET /api/v1/emails/:id
{
  "object": "email",
  "id": "cmf9x1v0000018lfr6zot86p",
  "from": "Acme <hello@example.com>",
  "to": ["jane@example.org"],
  "cc": [],
  "bcc": [],
  "reply_to": null,
  "subject": "Welcome to Acme",
  "created_at": "2026-08-17T14:03:11.120Z",
  "scheduled_at": null,
  "last_event": "delivered",
  "mode": "live",
  "events": [
    { "type": "sent", "recipient": "jane@example.org", "at": "2026-08-17T14:03:12.410Z" },
    { "type": "delivered", "recipient": "jane@example.org", "at": "2026-08-17T14:03:13.902Z" }
  ]
}

last_event is the message status in lower case: queued, sending, sent, delivered, bounced, complained, failed, suppressed, canceled, unconfirmed. An unconfirmed message left the provider, but no delivery event arrived; an operator closes such messages after an ingest outage. Each event has type (queued, sent, delivered, delivery_delayed, bounced, complained, rejected, opened, clicked, unsubscribed, suppressed, failed), recipient and at. When present, an event also has bounce_type (hard / soft / undetermined), detail (provider sub-type or failure text) and url (clicks).

mode is live or test (see below).

Test mode

A f5_test_ key makes each POST /emails a simulation. The message goes through the full contract (domain check, key restriction, suppression, idempotency, bulk rules). Then the worker, not Amazon SES, decides the outcome from the local part of each recipient:

Recipient Events
bounce@… sent, bounced (hard)
complaint@… sent, delivered, complained
suppressionlist@… sent, rejected
ooto@… or any other address sent, delivered

The domain part does not matter. The worker ignores a +label. Test messages show a Test pill in the console and report "mode": "test". They never touch metrics, warm-up, daily caps or the suppression list. Webhooks fire with the same payloads as live mail, so you can test your handlers end to end. A test key cannot create or send broadcasts, post events, or write contacts and subscriptions (403 test_key_not_allowed), because those paths start live mail from the worker.

List emails

GET /api/v1/emails?limit=25&cursor=…&status=delivered&to=jane@example.org&kind=invoice&mode=live&since=2026-08-01T00:00:00Z&until=2026-08-31T23:59:59Z

Returns summaries, newest first. Every filter is optional. limit is 1–100 (default 25). The response carries next_cursor when more pages exist; pass it back as cursor. The cursor is opaque; an invalid one answers 422 invalid_cursor. A domain-restricted key sees only the messages of its domains.

{
  "object": "list",
  "data": [
    { "id": "cmf9…", "from": "Acme <hello@example.com>", "to": ["jane@example.org"], "subject": "Welcome", "last_event": "delivered", "mode": "live", "kind": "welcome", "created_at": "2026-08-17T14:03:11.120Z", "sent_at": "2026-08-17T14:03:12.410Z" }
  ],
  "next_cursor": "MjAyNi0wOC0xN1QxNDowMzoxMS4xMjBafGNtZjk…"
}

Fetch /emails/:id for events and bodies.

Cancel an email

POST /api/v1/emails/:id/cancel

The call works while the message is queued (this includes scheduled sends). Response:

{ "id": "cmf9x1v0000018lfr6zot86p", "object": "email", "status": "canceled" }

Once the worker has picked the message up, or the platform has already sent it, the call returns 409 not_cancelable.