Back to Blog

How to Send Emails from Cloudflare Workers (2026 Guide)

10 min read

Cloudflare Workers run at the edge with the Web Standards API. Email sending is a fetch call to your provider's API. No npm packages needed for basic sending, though you can use them with Workers bundler.

This guide covers Workers email sending, environment bindings, and scheduled triggers.

Create a Worker

npm create cloudflare@latest -- send-email-worker
cd send-email-worker

Email Sending Worker

src/index.ts
interface Env {
SEQUENZY_API_KEY: string;
}

export default {
async fetch(request: Request, env: Env): Promise<Response> {
  if (request.method !== "POST") {
    return new Response("Method not allowed", { status: 405 });
  }

  const url = new URL(request.url);

  if (url.pathname === "/api/send-welcome") {
    const { email, name } = await request.json<{ email: string; name: string }>();

    if (!email || !name) {
      return Response.json({ error: "email and name required" }, { status: 400 });
    }

    const response = await fetch("https://api.sequenzy.com/v1/transactional/send", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${env.SEQUENZY_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        to: email,
        subject: `Welcome, ${name}`,
        body: `<h1>Welcome, ${name}</h1><p>Your account is ready.</p>`,
      }),
    });

    if (!response.ok) {
      return Response.json({ error: "Failed to send" }, { status: 500 });
    }

    return Response.json(await response.json());
  }

  return new Response("Not found", { status: 404 });
},
};
src/index.ts
interface Env {
RESEND_API_KEY: string;
}

export default {
async fetch(request: Request, env: Env): Promise<Response> {
  if (request.method !== "POST") {
    return new Response("Method not allowed", { status: 405 });
  }

  const url = new URL(request.url);

  if (url.pathname === "/api/send-welcome") {
    const { email, name } = await request.json<{ email: string; name: string }>();

    if (!email || !name) {
      return Response.json({ error: "email and name required" }, { status: 400 });
    }

    const response = await fetch("https://api.resend.com/emails", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${env.RESEND_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        from: "Your App <noreply@yourdomain.com>",
        to: email,
        subject: `Welcome, ${name}`,
        html: `<h1>Welcome, ${name}</h1><p>Your account is ready.</p>`,
      }),
    });

    if (!response.ok) {
      return Response.json({ error: "Failed to send" }, { status: 500 });
    }

    return Response.json(await response.json());
  }

  return new Response("Not found", { status: 404 });
},
};
src/index.ts
interface Env {
SENDGRID_API_KEY: string;
}

export default {
async fetch(request: Request, env: Env): Promise<Response> {
  if (request.method !== "POST") {
    return new Response("Method not allowed", { status: 405 });
  }

  const url = new URL(request.url);

  if (url.pathname === "/api/send-welcome") {
    const { email, name } = await request.json<{ email: string; name: string }>();

    if (!email || !name) {
      return Response.json({ error: "email and name required" }, { status: 400 });
    }

    const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${env.SENDGRID_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        personalizations: [{ to: [{ email }] }],
        from: { email: "noreply@yourdomain.com" },
        subject: `Welcome, ${name}`,
        content: [{ type: "text/html", value: `<h1>Welcome, ${name}</h1><p>Your account is ready.</p>` }],
      }),
    });

    if (!response.ok) {
      return Response.json({ error: "Failed to send" }, { status: 500 });
    }

    return Response.json({ sent: true });
  }

  return new Response("Not found", { status: 404 });
},
};

Set Secrets

wrangler secret put SEQUENZY_API_KEY

Scheduled Emails (Cron Triggers)

Send emails on a schedule using Workers Cron Triggers:

// src/index.ts
interface Env {
  SEQUENZY_API_KEY: string;
}
 
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // ... HTTP handler
  },
 
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
    // Runs on schedule - e.g., send a daily digest
    await fetch("https://api.sequenzy.com/v1/transactional/send", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${env.SEQUENZY_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        to: "team@yourcompany.com",
        subject: "Daily Report",
        body: "<h1>Daily Report</h1><p>Here are today's metrics...</p>",
      }),
    });
  },
};
# wrangler.toml
[triggers]
crons = ["0 9 * * *"]  # Every day at 9am UTC

Deploy

wrangler deploy

Going to Production

1. Verify Your Domain

Add SPF, DKIM, DMARC DNS records.

2. Use Secrets for API Keys

wrangler secret put SEQUENZY_API_KEY

Never put API keys in wrangler.toml.

3. Use ctx.waitUntil for Background Work

ctx.waitUntil(sendEmailInBackground(env));

This lets the response return immediately while the email sends in the background.

Beyond Transactional

Sequenzy handles transactional sends, marketing campaigns, automated sequences, and subscriber management from one API. Native Stripe integration for SaaS.

Wrapping Up

  1. Fetch API for zero-dependency email sending
  2. Environment bindings for secure API key access
  3. Cron triggers for scheduled emails
  4. Edge deployment for low-latency responses

Pick your provider, deploy a Worker, and start sending.