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.
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