Stateful & Async
Transactional Email
Send transactional email via the Cloudflare Email Service binding
Send transactional email via the Cloudflare Email Service send_email binding (EMAIL). Pair with Email Worker Inbox for the receive side.
API Reference
POST /send
Send an email. Provide text and/or html body content.
to string (required)
Recipient email address.
subject string (required)
Subject line.
text string (optional)
Plain-text body (at least one of text or html required).
html string (optional)
HTML body (at least one of text or html required).
Example Request
curl -X POST "https://your-worker.workers.dev/send" \
-H "Content-Type: application/json" \
-d '{"to":"[email protected]","subject":"Welcome","text":"Thanks for signing up."}'Success Response
{
"sent": true,
"to": "[email protected]",
"subject": "Welcome",
"messageId": "..."
}Error Codes
400- Invalid to, subject, body, or JSON (INVALID_TO,INVALID_SUBJECT,MISSING_BODY,INVALID_BODY)502- Email send failure (SEND_ERROR)
Use Cases
- Learn the Email Sending /
send_emailbinding - Transactional welcome, reset, and notification emails
- Restrict destinations with
destination_addressduring demos - Pair with Email Worker Inbox for end-to-end mail experiments
Limitations
- Requires onboarding a sending domain (
wrangler email sending enable) FROM_EMAILmust be on that domain- Optional
destination_addresson the binding can lock recipients for demos - No queueing or retry UI in this minimal experiment
Use in your project
Copy these files into an existing Worker. Prefer Deployment to try the full experiment first. Source: apps/experiments/transactional-email.
DependenciesNoneBindingsEmailPlatformWorkers runtime
import { DEFAULT_FROM_EMAIL, EMAIL_PATTERN, MAX_BODY_LENGTH, MAX_SUBJECT_LENGTH,} from "../constants/defaults";import type { Env } from "../types/env";import type { SendEmailRequest, SendEmailResponse } from "../types/email";export function validateEmail(input: string | undefined): string | null { if (!input || typeof input !== "string") return null; const trimmed = input.trim(); if (!EMAIL_PATTERN.test(trimmed)) return null; return trimmed;}export function validateSubject(input: string | undefined): string | null { if (!input || typeof input !== "string") return null; const trimmed = input.trim(); if (!trimmed || trimmed.length > MAX_SUBJECT_LENGTH) return null; return trimmed;}export function validateBodyField(input: string | undefined): string | null { if (input === undefined || input === null) return null; if (typeof input !== "string") return null; if (!input.trim() || input.length > MAX_BODY_LENGTH) return null; return input;}export async function sendTransactionalEmail( env: Env, request: Required<Pick<SendEmailRequest, "to" | "subject">> & Pick<SendEmailRequest, "text" | "html">): Promise<SendEmailResponse> { const from = env.FROM_EMAIL?.trim() || DEFAULT_FROM_EMAIL; const result = await env.EMAIL.send({ from, to: request.to, subject: request.subject, text: request.text, html: request.html, }); return { sent: true, to: request.to, subject: request.subject, messageId: result.messageId, };}Deployment
Configure Email Sending
- Onboard a sending domain:
npx wrangler email sending enable yourdomain.com - Set
FROM_EMAILinwrangler.jsonvarsto an address on that domain - Optionally restrict destinations with
destination_addresson theEMAILbinding
Test your deployment
curl -X POST "https://your-worker.workers.dev/send" \
-H "Content-Type: application/json" \
-d '{"to":"[email protected]","subject":"Welcome","text":"Thanks for signing up."}'Local Development
cd apps/experiments/transactional-email
npm install
npm run devcurl -X POST "http://localhost:8787/send" \
-H "Content-Type: application/json" \
-d '{"to":"[email protected]","subject":"Welcome","text":"Thanks for signing up."}'Configuration
wrangler.json declares:
send_emailbindingEMAIL(optionaldestination_address)- Var
FROM_EMAIL
Cloudflare Features Used
- Workers - Edge compute runtime
- Email Sending -
send_emailbinding for outbound mail