How to Send Email from Cloudflare Workers
DEV Community

How to Send Email from Cloudflare Workers

There are two real ways to do this: Cloudflare's own native Email Service binding, or calling an external email API like Notify over fetch(). I'll walk through both, but I want to flag something about the native option up front that's easy to miss until you're actually setting it up: it currently requires the Workers Paid plan, not just a Cloudflare account. If you're on the free Workers tier and just want to send a password reset email, that's worth knowing before you spend time on it.

Option 1: Cloudflare's Native Email Service Binding

Cloudflare's Email Service (which covers both sending and receiving) lets a Worker send email through a binding, with no external API key. As of now it's still in beta, and there's a real gate on it: sending to arbitrary recipients requires the Workers Paid plan, and before you've fully onboarded a domain, the binding can only send to destination addresses you've explicitly verified.

Setup looks like this:

  • In the Cloudflare dashboard, go to Compute > Email Service > Email Sending, click Onboard Domain, and pick the domain you want to send from.
  • Cloudflare adds the DNS records it needs automatically - an SPF record, a DKIM record, a DMARC record, and MX records on a cf-bounce subdomain.
  • This usually finishes in minutes, though Cloudflare says it can take up to 24 hours.

Add the binding to your Wrangler config:

{ "send_email": [{ "name": "EMAIL" }] }

Send from your Worker:

export default {
  async fetch(request, env, ctx) {
    await env.EMAIL.send({
      from: "n******@yourdomain.com",
      to: "u***@example.com",
      subject: "Welcome!",
      html: "<h1>Thanks for signing up.</h1>",
    });
    return new Response("Email sent!", { status: 200 });
  },
};

A couple of things worth knowing before you build on this: by default, wrangler dev simulates the binding locally - emails are logged to your console, not actually sent - unless you set remote: true on the binding to send real mail during local development. And you can restrict which senders and recipients the binding is allowed to use (allowedSenderAddresses, allowedDestinationAddresses), which is worth doing regardless of which sending method you pick.

Option 2: Calling an External Email API Over fetch()

This is the more portable pattern, and it's worth noting that it fits the Workers runtime particularly well for a specific reason: Workers run on a V8 isolate, not Node.js, so any library that assumes Node-specific built-ins can quietly break in ways that are annoying to debug. A plain HTTP API you call with fetch() has none of that risk, since there's no package to be incompatible in the first place - which is exactly the shape Notify is, since it has no SDK at all.

  • Store your API key as a secret: wrangler secret put NOTIFY_API_KEY
  • Call the API from your Worker:
export default {
  async fetch(request, env, ctx) {
    const response = await fetch("https://notify.cx/api/email/send", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-api-key": env.NOTIFY_API_KEY,
      },
      body: JSON.stringify({
        to: "u***@example.com",
        from: "n******@your-verified-domain.com",
        subject: "Welcome!",
        message: "<h1>Thanks for signing up.</h1>",
      }),
    });
    if (!response.ok) {
      const text = await response.text();
      return new Response(`Email send failed: ${text}`, { status: 500 });
    }
    return new Response("Email sent!", { status: 200 });
  },
};

Environment variables and secrets work exactly the way they do for any other Worker - env.NOTIFY_API_KEY here is no different from any other secret you'd reference, which is part of why this integration doesn't feel like a special case once you've set up a Worker with any external API before. That's the entire integration - no binding to configure in Wrangler, no domain onboarding through Cloudflare's dashboard specifically (you still verify a domain with Notify directly, via standard SPF/DKIM/DMARC DNS records), and it works identically whether this Worker is the only thing calling Notify or you've also got a Node backend doing the same thing elsewhere.

Reacting to Bounces from a Worker

If you want to know when an email fails without polling, register a webhook once - this isn't something the native Cloudflare binding gives you an equivalent of without building your own event handling:

curl -X POST https://notify.cx/api/webhooks \
  -H "Content-Type: application/json" \
  -H "x-api-key: $NOTIFY_API_KEY" \
  -d '{ "webhookUrl": "https://yourworker.example.com/webhooks/email", "subscribedEvents": ["Bounce", "Delivery"], "domainId": "your-domain-id" }'

If you want the

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.