Send Email from Cloudflare Workers
Send email from a Cloudflare Worker with Wraps. Enable nodejs_compat, store AWS credentials as Wrangler secrets, and send from the edge.
Deploy email infrastructure to your AWS account and send your first email from a Cloudflare Worker running at the edge.
Run this quickstart with your AI agent
Paste into Claude Code, Cursor, or any agent with shell access.
Using Claude Code? Install the Wraps skills for deeper context:
Before you begin, make sure you have:
Run the Wraps CLI to deploy email infrastructure to your AWS account:
npx @wraps.dev/cli email initAdd and verify your sending domain with AWS SES:
npx @wraps.dev/cli email domains add -d yourdomain.comDNS Setup
The CLI will output DKIM records to add to your DNS provider. Once added, verify them with npx @wraps.dev/cli email domains verify -d yourdomain.com
Scaffold a new Worker if you don't have one, then install the @wraps.dev/email package:
npm create cloudflare@latest email-worker -- --type hello-worldcd email-worker && npm install @wraps.dev/emailThe @wraps.dev/email/workers entry uses Web Crypto and fetch— no Node.js APIs — so you don't need nodejs_compat. Just set a region variable in your Wrangler config:
{ "name": "email-worker", "main": "src/index.ts", "compatibility_date": "2026-06-16", "vars": { "AWS_REGION": "us-east-1" }}No Node.js Compat Needed
The /workers entry is self-contained (~5 KB bundled) and runs on plain workerd with no polyfills. Drop the nodejs_compat flag entirely — it's not required and adds unnecessary overhead.
Workers have no filesystem and no AWS credential chain, so you can't rely on ~/.aws or instance metadata. Store your IAM keys as encrypted Wrangler secrets:
npx wrangler secret put AWS_ACCESS_KEY_IDnpx wrangler secret put AWS_SECRET_ACCESS_KEYLocal Development
For wrangler dev, put the same keys in a .dev.vars file (and add it to .gitignore). Scope the IAM user to ses:SendEmail only.
Build a WrapsEmail instance inside the fetch handler, passing credentials from the env binding. The handler below accepts a JSON body and returns the SES message ID:
import { SESError, ValidationError, WrapsEmail } from '@wraps.dev/email/workers';type Env = { AWS_ACCESS_KEY_ID: string; AWS_SECRET_ACCESS_KEY: string; AWS_REGION: string;};export default { async fetch(request: Request, env: Env): Promise<Response> { // Instantiate per request — `env` isn't available at module scope. // Workers have no AWS credential chain (no filesystem, no ~/.aws, // no instance metadata), so pass credentials explicitly from the env // binding — Wrangler secrets for the keys, a var for the region. const email = new WrapsEmail({ region: env.AWS_REGION, credentials: { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY, }, }); try { const { to, subject, html } = await request.json<{ to: string; subject: string; html: string; }>(); const result = await email.send({ from: 'hello@yourdomain.com', to, subject, html, }); return Response.json({ success: true, messageId: result.messageId }); } catch (error) { if (error instanceof ValidationError) { return Response.json({ error: error.message }, { status: 400 }); } if (error instanceof SESError) { return Response.json( { error: error.message, code: error.code }, { status: error.retryable ? 503 : 400 }, ); } throw error; } },};Error Types
retryable to decide whether to retryShip the Worker to Cloudflare's edge network:
npx wrangler deploySend a test request to your Worker URL:
curl -X POST https://email-worker.<your-subdomain>.workers.dev \ -H "Content-Type: application/json" \ -d '{"to":"you@example.com","subject":"Hello from the edge","html":"<h1>It works</h1>"}'Learn about all available methods, options, and advanced features.
View SDK DocsReference for all error codes and troubleshooting steps.
View Errors