Skip to content

tutorials

MailChannels for Cloudflare Workers is gone: the alternative

July 24, 2026

#cloudflare-workers#mailchannels#email#smtp#tutorial
MailChannels for Cloudflare Workers is gone: the alternative
Visual: Konstruktor Ivanova on okslop

You copied a 2023 tutorial, pasted a fetch("https://api.mailchannels.net/...") into your Worker, and got a 401. The free ride ended. Here is what actually sends mail from a Worker now.

For about three years, the answer to "how do I send email from a Cloudflare Worker" was a single, almost magical snippet. No account, no API key, no credit card: you POSTed JSON to api.mailchannels.net/tx/v1/send and a transactional email went out. That worked because of a partnership between Cloudflare and MailChannels that gave Workers a free relay.

That partnership ended. As of June 30, 2024, the no-account MailChannels route stopped accepting Worker traffic. Every tutorial built on it is now dead code. If you are arriving here because your old snippet started returning errors, you are not doing anything wrong: the road was closed.

This post does three things. First, it confirms exactly what broke so you can stop debugging your own code. Then it walks the real alternatives the hard way, with working requests and the DNS changes each one needs. Finally, it covers the option that keeps the whole thing on your own Cloudflare account.

What exactly broke when MailChannels deprecated the Workers route?

MailChannels retired the free, partnership-based relay for Cloudflare Workers on June 30, 2024. The specific thing that died is the ability to call api.mailchannels.net/tx/v1/send from a Worker with no authentication. That endpoint relied on Cloudflare's network identifying the caller as a Worker and MailChannels trusting that signal for free. Both halves of that arrangement are gone.

Here is the snippet that used to work and what it returns today:

// The classic free MailChannels send. This no longer works.
export default {
  async fetch(): Promise<Response> {
    const res = await fetch("https://api.mailchannels.net/tx/v1/send", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        personalizations: [{ to: [{ email: "user@example.com" }] }],
        from: { email: "hello@yourdomain.com" },
        subject: "Hello",
        content: [{ type: "text/plain", value: "Test" }],
      }),
    });
    return new Response(`status ${res.status}`);
  },
};
status 401
{ "errors": ["No valid API key found. See https://support.mailchannels.com ..."] }

That 401 (sometimes a 403, sometimes a 404 depending on the path) is the whole story. MailChannels did not disappear: it is still a real, working email API. But it is now an ordinary paid product that requires an account and an API key, like everyone else. The free, zero-config era is over.

If you only need to fix the immediate breakage and you are happy to stay with MailChannels, the migration is small: sign up, get an API key, and add an X-Api-Key header. We will cover that as option 1 below. But this is also a good moment to reconsider where your mail goes out from at all, so let's look at the full menu.

What are my options to send email from a Worker now?

You have three real paths, and one tempting dead end. The dead end is SMTP, so let's kill it first because it saves you hours.

Why you cannot just switch to SMTP

You cannot deliver direct-to-MX SMTP from a Worker, and Cloudflare's own SMTP submission endpoint is not a Worker transport. There are two independent network boundaries:

  1. Workers sockets are not Node sockets. Workers run on workerd, not a full Node process. They expose the Workers connect() API for outbound TCP, while Node SMTP packages are not automatically compatible with it. A deliberately Worker-native client may reach an allowed third-party submission service.
  2. Port 25 is blocked at the network level. Open port 25 egress is the internet's biggest spam vector, so Cloudflare (like AWS, GCP, and most hosts) blocks it. Even with a socket, you could not reach a recipient's MX server.

This is why MailChannels existed in the first place: it did the SMTP-to-the-world part on infrastructure that is allowed to, and exposed an HTTPS endpoint to Workers. For Cloudflare Email Service specifically, use the native binding or REST API from a Worker: smtp.mx.cloudflare.net:465 resolves to Cloudflare-owned IPs, which Workers TCP cannot reach. A tested third-party SMTP submission service is a separate possibility, not a route to recipient MX servers. If you want the full menu of approaches before committing, the ways to send email from Cloudflare Workers lays them side by side, and the myths about sending email from Cloudflare Workers clears up the false starts people waste a day on. With that settled, the three live options are:

  • Option 1: Keep MailChannels, but as a paid account with an API key.
  • Option 2: Switch to a hosted email API (Resend, Postmark, Amazon SES, and so on).
  • Option 3: Run your own HTTPS sender on your own Cloudflare account.

How do I fix it by staying on MailChannels (the smallest change)?

Create a MailChannels account, generate an API key, and add it as an X-Api-Key header on the same request. The payload shape is unchanged, so this is the least invasive fix.

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const res = await fetch("https://api.mailchannels.net/tx/v1/send", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "X-Api-Key": env.MAILCHANNELS_API_KEY,
      },
      body: JSON.stringify({
        personalizations: [{ to: [{ email: "user@example.com" }] }],
        from: { email: "hello@yourdomain.com", name: "Your App" },
        subject: "Reset your password",
        content: [{ type: "text/html", value: "<p>Click to reset.</p>" }],
      }),
    });
    if (!res.ok) return new Response(await res.text(), { status: 502 });
    return new Response("sent");
  },
};

Set the secret out of band so it is not in your repo:

npx wrangler secret put MAILCHANNELS_API_KEY

You also need to fix DNS. MailChannels uses a domain lockdown TXT record so that only your account can send as your domain, plus the SPF include:

TXT  _mailchannels   v=mc1 auth=your-account-handle
TXT  @               v=spf1 include:relay.mailchannels.net ~all

The honest read on this option: it is the fastest fix, and MailChannels deliverability is solid. The downside is that you have traded a free magic route for a paid vendor account, and you are now back in the same boat as every other email SaaS, with your sending reputation tied to their platform. If you are going to pay and manage an account anyway, it is worth comparing the alternatives rather than defaulting to the incumbent out of inertia.

How do I switch to a hosted email API like Resend or Postmark?

Pick a provider, verify your domain in their dashboard, get an API key, and replace the MailChannels fetch with theirs. This is the most common migration and it works well. Here is the Resend version, which is representative:

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const res = await fetch("https://api.resend.com/emails", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        authorization: `Bearer ${env.RESEND_API_KEY}`,
      },
      body: JSON.stringify({
        from: "Your App <hello@yourdomain.com>",
        to: ["user@example.com"],
        subject: "Reset your password",
        html: "<p>Click to reset.</p>",
      }),
    });
    if (!res.ok) return new Response(await res.text(), { status: 502 });
    return new Response("sent");
  },
};

A failure you will likely hit on the first try, before you finish DNS:

{
  "statusCode": 403,
  "name": "validation_error",
  "message": "The yourdomain.com domain is not verified. Add the required DNS records and try again."
}

The fix is the DNS dance every provider makes you do. You remove the MailChannels records from the previous section and add the new sender's records instead. For a typical API provider that means an SPF include, a DKIM key (often as a CNAME pointing at their key, or a TXT record), and a DMARC policy:

TXT    @                 v=spf1 include:_spf.theprovider.com ~all
CNAME  resend._domainkey resend._domainkey.theprovider.com
TXT    _dmarc            v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com

This is a genuinely good path. Resend and Postmark have excellent developer experience, their dashboards show you delivery and bounce events, and you stop maintaining sending infrastructure. We have written an honest take on exactly when their DX is worth it and when it is not in when a Resend alternative makes sense, and if you are shopping more widely, the transactional email API alternatives worth comparing and the best Amazon SES alternatives cover the rest of the field.

The tradeoff is the same one MailChannels-as-a-paid-account has, just with a nicer UI. Your sending domain, your DKIM private key, your subscriber data, and your reputation history all live on the vendor. Pricing is usually per-email or per-month-tier, which is fine until volume climbs; we break down those curves in the real cost of transactional email. If that pattern is what you are trying to get away from, there is a third option.

How do I run my own sender on my own Cloudflare account?

Run an HTTPS sender inside your own Cloudflare account, so the Worker still makes an ordinary HTTPS-shaped call but the delivery infrastructure (DKIM signing, queueing, bounce handling) belongs to you instead of a vendor. This is the path that most directly replaces what MailChannels gave you: a sender that lives next to your Workers, except now it is on your account and there is no per-email vendor in the middle.

You could build this yourself. It is a real project: a consumer Worker that signs messages with DKIM, a queue so a single request does not block on delivery, a relay that is permitted to talk to recipient MX servers over a path that is not blocked, bounce and complaint parsing, and a suppression list so you stop mailing dead addresses. None of it is exotic, but it is weeks of work and ongoing maintenance, which is precisely why hosted APIs exist.

Or you skip all of that and deploy a sender that is already built, onto your own Cloudflare account. That is what Tinbox is. One command provisions the resources and prints the DNS records you need:

npx tinbox init yourdomain.com
Provisioned D1 database (tinbox-yourdomain-db)
Provisioned R2 bucket   (tinbox-yourdomain-mail)
Provisioned Queue       (tinbox-yourdomain-send)

Add these DNS records to yourdomain.com:

  TXT  @                   v=spf1 include:_spf.yourdomain.com ~all
  TXT  tinbox._domainkey   v=DKIM1; k=rsa; p=MIIBIjANBg...
  TXT  _dmarc              v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com

Once DNS propagates, run: npx tinbox doctor

Then the send from your Worker is a binding call, not a fetch to someone else's domain:

import { tinbox } from "tinbox/cf";

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    await tinbox(env).send({
      to: "user@example.com",
      from: "hello@yourdomain.com",
      subject: "Reset your password",
      html: `<p>Click <a href="https://app.yourdomain.com/reset?t=abc">here</a> to reset.</p>`,
    });
    return new Response("sent");
  },
};

Under the hood tinbox(env).send(...) enqueues the message onto a Cloudflare Queue in your account and returns immediately, so the request stays fast. A consumer Worker signs it with DKIM and performs delivery, handling retries and bounce parsing, and writing the results back into your own D1 database. There is no MailChannels, no Resend, and no per-email invoice from a third party: the message goes out over your Cloudflare account, and the bounce data, suppression list, and DKIM keys are yours.

To be precise about the tradeoff so you can decide clearly: with this path you are responsible for your own sending reputation and your own DNS, the same as you would be with a paid MailChannels or SES account, and you do the warmup yourself. What you get back is that nothing about your mail pipeline lives on a vendor that can deprecate a route out from under you, the way the free MailChannels relay just did. If reputation and warmup are new to you, start with the email deliverability guide before you send to a real list.

Common failure modes after the migration

A few errors you will see while switching, and what each one means:

401 / 403 from api.mailchannels.net

The free route, with no API key. This is the deprecation itself. Either add an X-Api-Key for a paid MailChannels account or switch senders entirely.

Mail accepted but landing in spam

Your DNS still points at the old sender, or DKIM is not aligned with your new one. You almost certainly left the old _mailchannels lockdown or SPF include in place, or your new DKIM record has not propagated. Check that SPF, DKIM, and DMARC all resolve for the sender you are actually using now. This walkthrough covers the records end to end.

550 5.7.1 Sender address rejected: domain not verified

Your from domain is not authenticated with the new sender. You can only send as a domain you control and have DKIM-signed with the sender that is delivering the message.

Sending works in dev, fails in deployed Worker

Usually a missing secret or binding. The API key you set locally was not pushed (wrangler secret put), or the queue binding for a self-hosted sender is not in wrangler.jsonc.

The short version

MailChannels closed its free Cloudflare Workers relay on June 30, 2024, so any tutorial that POSTs to api.mailchannels.net with no API key is broken. Direct SMTP delivery to recipient MX servers remains impossible because port 25 is blocked. Prefer a provider HTTPS API or Cloudflare's Email Service binding/REST API; a Worker-native TCP client to an allowed third-party submission service is possible but is a separate compatibility and operations choice. Cloudflare's own authenticated SMTP endpoint on port 465 is intended for external clients and is unreachable from Workers because Cloudflare-owned IP ranges are blocked.

Deploy this yourself: npx tinbox init yourdomain.com, and see the docs for the full sender, DNS, and bounce-handling setup.

Frequently asked questions

Why did MailChannels stop working from Cloudflare Workers?

MailChannels ended its free, partnership-based relay for Cloudflare Workers on June 30, 2024. The route that let any Worker POST to api.mailchannels.net with no account now returns 401/403. MailChannels still exists as a paid product, but it now requires an account and an API key, so the old zero-config tutorials no longer work.

What is the best MailChannels alternative for Cloudflare Workers in 2026?

Any sender that accepts an HTTPS request works from a Worker: Resend, Postmark, Amazon SES, or your own sender. The differentiator is ownership. A hosted API keeps your sending domain and reputation on a vendor's account; running your own HTTPS sender on your Cloudflare account (for example with Tinbox) keeps the whole pipeline, DKIM keys, and bounce data on infrastructure you control.

Can I send email over SMTP from a Cloudflare Worker instead?

Workers expose outbound connect() sockets, but port 25 and Cloudflare-owned IP ranges are blocked. Node SMTP library compatibility is separate from that network boundary. Use a provider HTTPS API or the Email Service binding/REST path from a Worker; authenticated Cloudflare SMTP on port 465 is for external clients.

Do I need to change my DNS records after moving off MailChannels?

Yes. MailChannels required a specific SPF include and a _mailchannels TXT lockdown record. When you switch senders you remove those and add the SPF, DKIM, and DMARC records your new sender provides, or your mail will fail authentication and land in spam.