Send email with Cloudflare Workers

Send from the edge with fetch, and keep the key in a secret binding.

You need a verified sending domain and an API key from Settings › API Keys. If neither exists yet, the quickstart covers both in a couple of minutes.

Send

export interface Env {
  SENDING_API_KEY: string;
}
 
export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const { email, userId } = await req.json<{ email: string; userId: string }>();
    const res = await fetch("https://sending.dev/api/v1/emails", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${env.SENDING_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        from: "Acme <[email protected]>",
        to: email,
        subject: "Welcome",
        html: "<p>You're in.</p>",
        idempotencyKey: `welcome-${userId}`,
      }),
    });
    return new Response(await res.text(), { status: res.status });
  },
};

A successful call answers 202 with the message id: accepted and queued, not yet delivered. Delivery, opens, bounces and complaints arrive later, on webhooks or in the dashboard.

What changes in Cloudflare Workers

Set the key with wrangler secret put SENDING_API_KEY: a secret binding is not in wrangler.toml and does not end up in the repository.

The thing that catches people

Workers cannot use Node crypto by default, which matters for verifying webhook signatures. Use crypto.subtle with HMAC SHA-256, or enable the nodejs_compat flag.

Next

  • All the fields: cc and bcc, reply-to, attachments, templates, scheduling.
  • Webhooks: delivery, bounce and complaint events, signed with HMAC.
  • Domains: SPF, DKIM and MAIL FROM, and why sending is refused until they are in place.
  • MCP: the same operations as tools, when the one writing the code is an agent.

Nearby: Platforms and edge runtimes

All 30 stacks