Quickstart
#quickstartCopy the example env file and set the required variables. CF_API_TOKEN and CF_ACCOUNT_ID come from Cloudflare. Generate random secrets for ADMIN_TOKEN and SESSION_SECRET.
# Minimum required
ADMIN_TOKEN=$(openssl rand -hex 32)
SESSION_SECRET=$(openssl rand -hex 32)
CF_API_TOKEN=your_cloudflare_api_token
CF_ACCOUNT_ID=your_account_id
Use the production compose stack. The GHCR image is pulled automatically — no local build needed.
docker compose --env-file .env.local -f compose.yaml up -d
# Local dev (includes Mailpit — no real emails sent)
docker compose --env-file .env.local -f compose.dev.yaml up
# → 200 OK
# Admin dashboard
http://localhost:8090
Add a Cloudflare sending domain in the Domains page, then generate an API key from the Keys page. Use a test key first to send via SMTP without real Cloudflare delivery.
Authentication
#authEvery request to /v1/send requires a bearer token. Use an EmailFlare API key, never your Cloudflare root token.
Content-Type: application/json
Key types
Prefix: eftest_. Sends go through SMTP — no Cloudflare Email Sending credentials required. Safe for local and staging.
Prefix: eflive_. Sends go through Cloudflare Email Sending. Requires valid CF_API_TOKEN and verified domain.
403.
POST /v1/send
#api-sendThe single endpoint for all transactional sends. Use templateSlug / templateId for stored templates, or pass raw html / text directly.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
from |
string | required | Sender email address. Must match a verified domain authorized for your key. |
fromName |
string | optional | Display name shown in the From header, e.g. Acme Inc. |
to |
string or string[] | required | Recipient address or array of up to 50 addresses. Duplicates are deduplicated. |
subject |
string | optional | Overrides the template subject. Required for raw html/text sends. |
templateSlug |
string | conditional | Stable slug-based template reference. Preferred for application code. |
templateId |
string | conditional | Internal ID-based template reference. Use templateSlug when possible. |
themeId |
string | optional | Applies a built-in colour theme to layout-based templates. See theme list below. Defaults to default. |
variables |
object | optional | Key/value map for {{variable}} interpolation in subject and body. |
html |
string | conditional | Raw HTML body. Used when not referencing a template. |
text |
string | conditional | Plain-text body. Can be combined with html. |
replyTo |
string | optional | Reply-to address for the recipient. |
templateSlug, templateId, html, or text must be provided.
cURL
-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)
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"
}]
}
Templates & themes
#templatesEmailFlare 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.
Available themes
defaultoceanforestvioletslateTemplate reference tips
- Prefer
templateSlugovertemplateId— 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 the literal
{{variableName}}placeholder. themeIdonly applies to layout-based templates. Custom HTML templates ignore it.
Error responses
#errors| Status | Meaning | Common cause |
|---|---|---|
| 401 | Unauthorized | Missing or invalid Authorization header. |
| 403 | Forbidden | Key scope does not authorize the sender domain. |
| 404 | Not found | templateSlug or templateId does not exist. |
| 422 | Validation error | Missing required fields or invalid email format. |
| 429 | Rate limited | Per-key send limit reached. Check X-RateLimit-Reset header. |
| 502 | All recipients failed | Downstream delivery failed for every address in the request. |
| 500 | Server error | Unexpected backend error — check container logs. |
Rate limiting headers returned on every /v1/send response:
X-RateLimit-Remaining: 97
X-RateLimit-Reset: 1746403200
Troubleshooting
#troubleshooting401 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 use SMTP 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 →
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.
docker compose logs -f emailflare. Most issues are visible in the startup or request output.