Documentation v1

From zero to first delivered email in minutes.

Complete API reference — authentication, /v1/send, templates, themes, error codes, and troubleshooting. Everything you need to integrate with EmailFlare.

Quickstart

#quickstart

Get to a working send in four steps — from a fresh deploy to a delivered email.

1
Deploy EmailFlare — on Cloudflare Workers (primary) or Docker Compose (secondary). See the Cloudflare Workers guide or the self-hosting docs.
2
Add a sending domain — go to the Domains page in the admin UI, enter your domain, and complete the DNS verification steps Cloudflare provides.
3
Create an API key — go to the Keys page in the admin UI, click New key, and choose a scope. Copy the key — it won't be shown again.
4
Send your first emailPOST /v1/send with your key in the Authorization: Bearer header. Start with a test key and eftest_ prefix to skip Cloudflare credentials.

Authentication

#authentication

All /v1/send requests must include a bearer token in the Authorization header.

Authorization: Bearer eflive_xxxxxxxxxxxxxxxx

Key types

⚗ test key

Prefix: eftest_. Sends are stored in the admin dashboard — no Cloudflare credentials required. Browse, preview HTML, and manage test emails from the built-in Test Mailbox page.

✓ live key

Prefix: eflive_. Sends go through Cloudflare Email Sending. Requires valid CF_API_TOKEN and verified domain.

💡Domain-scoped keys can only send from domains listed in that key's permissions. Requests with an unauthorized domain return 403.

POST /v1/send

#api-send

The single endpoint for all transactional sends. Use templateSlug / templateId for stored templates, or pass raw html / text directly.

Request fields

FieldTypeRequiredDescription
fromstringrequiredSender email address. Must match a verified domain authorized for your key.
fromNamestringoptionalDisplay name shown in the From header, e.g. Acme Inc.
tostring or string[]requiredRecipient address or array of up to 50 addresses. Duplicates are deduplicated.
subjectstringoptionalOverrides the template subject. Required for raw html/text sends.
templateSlugstringconditionalStable slug-based template reference. Preferred for application code.
templateIdstringconditionalInternal ID-based template reference. Use templateSlug when possible.
themeIdstringoptionalApplies a built-in colour theme to layout-based templates. See theme list below. Defaults to default.
variablesobjectoptionalKey/value map for {{variable}} interpolation in subject and body. Supports Handlebars loops and conditionals — see templates.
htmlstringconditionalRaw HTML body. Used when not referencing a template.
textstringconditionalPlain-text body. Can be combined with html.
replyTostringoptionalReply-to address for the recipient.
listIdstringoptionalAudience list ID. Attaches a one-click List-Unsubscribe header with a per-recipient token, so the recipient can unsubscribe (globally suppressed for all future sends).
listUnsubscribestringoptionalOverride the unsubscribe URL (e.g. your own https://…/unsubscribe). Passed through verbatim.
listUnsubscribePostbooleanoptionalAdds List-Unsubscribe-Post: List-Unsubscribe=One-Click (RFC 8058). Defaults to true when listId is set.
At least one of templateSlug, templateId, html, or text must be provided.

cURL

curl -X POST https://your-emailflare.com/v1/send \ -H "Authorization: Bearer eflive_xxx" \ -H "Content-Type: application/json" \ -d '{"from": "hello@yourdomain.com","fromName": "Acme","to": "alex@example.com","templateSlug": "welcome","themeId": "ocean","variables": { "name": "Alex", "appName": "Acme" }'}

fetch (JavaScript)

const res = await fetch('https://your-emailflare.com/v1/send', {method: 'POST',headers: {'Authorization': 'Bearer eflive_xxx','Content-Type': 'application/json',},body: JSON.stringify({from: 'hello@yourdomain.com',to: 'alex@example.com',templateSlug: 'welcome',themeId: 'ocean',variables: { name: 'Alex', appName: 'Acme' },}),});const data = await res.json();

Response (200)

{"results": [{"to": "alex@example.com","cfId": "cf_msg_xxxxxxxxxxxxxxxxxx"}]}

Unsubscribe

#api-unsubscribe

EmailFlare supports one-click unsubscribe via List-Unsubscribe headers (RFC 8058). When a send includes a listId, a per-recipient, one-time token is embedded in the header. Recipients who click it are suppressed from all future sends.

Public endpoint

The unsubscribe endpoint is public — no API key required:

GET https://your-emailflare.com/v1/unsubscribe?token=<token>POST https://your-emailflare.com/v1/unsubscribe?token=<token>

GET returns a confirmation page (for browser clicks); POST returns JSON (for mail-client one-click). Each token is single-use.

Admin API

Manage lists with the admin API (session-authenticated, same as other /api/* routes):

GET /api/lists # list all listsPOST /api/lists # create a listDELETE /api/lists/:id # delete a list
Unsubscribe links require PUBLIC_URL (or public_url in config.toml) to be set. Without it, listId is ignored — pass your own listUnsubscribe URL instead.

Templates & themes

#templates

EmailFlare ships 50 layout-based templates built with React Email. Reference them by their templateSlug. Pass themeId to switch colour palettes per send — no CSS edits needed. For custom templates, see the templates guide →

Available themes

default
ocean
forest
violet
slate
💡Use the admin Playground to preview any template with any theme before sending to real recipients.

Template reference tips

  • Prefer templateSlug over templateId — slugs are stable across restores.
  • Variable names follow the {{camelCase}} convention used in the template body and subject.
  • If a variable is missing from the payload, it renders as an empty string — not as the literal {{variableName}} placeholder.
  • Custom templates use Handlebars, so loops ({{#each}}) and conditionals ({{#if}}) work. See the templates guide →
  • themeId only applies to layout-based templates. Custom HTML templates ignore it.

Error responses

#errors
StatusMeaningCommon cause
401UnauthorizedMissing or invalid Authorization header.
403ForbiddenKey scope does not authorize the sender domain.
404Not foundtemplateSlug or templateId does not exist.
422Validation errorMissing required fields or invalid email format.
429Rate limitedPer-key send limit reached. Check X-RateLimit-Reset header.
502All recipients failedDownstream delivery failed for every address in the request.
500Server errorUnexpected backend error — check container logs.

Rate limiting headers returned on every /v1/send response:

X-RateLimit-Limit: 100X-RateLimit-Remaining: 97X-RateLimit-Reset: 1746403200

Troubleshooting

#troubleshooting

401 on /v1/send

Confirm the header is exactly Authorization: Bearer <key> with no extra spaces. Verify the key is marked active on the Keys page.

403 with a domain-scoped key

The from address domain must be included in the key's allowed domains. Create a global scoped key to bypass domain restrictions while debugging.

Template not found (404)

Copy the slug exactly from the Templates page — it is case-sensitive. System templates use slugs like welcome, magic-link, otp.

Theme not applied to email

themeId only works with layout-based (built-in) templates. For custom HTML templates the field is ignored — you control the styles directly in the HTML body.

Sends succeed in test mode but fail in live mode

Test keys (eftest_) store emails in the dashboard and don't require Cloudflare credentials. Live keys use Cloudflare Email Sending — ensure CF_API_TOKEN has the correct permissions and the sender domain is verified. See the Cloudflare token guide →

Test emails are stored in the built-in Test Mailbox page in the admin UI — no external service needed.

Database errors in logs

If you see "This SQL statement is not allowed on /query endpoint", upgrade to the latest EmailFlare image — this was a bug fixed in the backend send route.

📋Always check container logs first: docker compose logs -f emailflare. Most issues are visible in the startup or request output.