MailKite
Get your API key
All guides Inbound email parsing

Cloudflare Email Routing

Inbound parsing turns the email your users send into structured data your app can act on: a reply becomes a support ticket, a forwarded PDF becomes a row in your database, an incoming message becomes a task for an agent. This guide walks through Cloudflare Email Routing and Email Workers end to end, then shows the same flow on MailKite.

What you'll need

  • A domain on Cloudflare — Email Routing requires the zone to use Cloudflare DNS.
  • A public HTTPS endpoint to receive the parsed message (a tunnel like ngrok works while testing).
  • The Wrangler CLI for the Worker in the first half.

Part 1 — Parse inbound email with Cloudflare

1. Enable Email Routing on your zone

In the Cloudflare dashboard, open Compute › Email Service › Email Routing and onboard your domain. Cloudflare adds the required DNS records to the root domain for you — the MX records that point mail at Cloudflare, plus an SPF (TXT) record and a routing DKIM record:

DNS — added automatically
Name             Type   Priority   Value
yourdomain.com MX (auto) route1.mx.cloudflare.net
yourdomain.com MX (auto) route2.mx.cloudflare.net
yourdomain.com MX (auto) route3.mx.cloudflare.net
yourdomain.com TXT — "v=spf1 include:_spf.mx.cloudflare.net ~all"

Priorities are assigned by Cloudflare, and the records are locked to prevent accidental edits. On Cloudflare DNS this usually resolves within a few minutes. Once it does, mail for every address on the domain flows into Email Routing.

2. Create a routing rule

A rule decides what happens to each incoming message. You can forward an address to a verified destination mailbox — the destination has to confirm a verification email first — or, to process mail in code, choose Send to a Worker and bind an Email Worker to the address (or set it as the catch-all). Deploy the Worker with Wrangler so the rule has something to point at:

wrangler.toml
# wrangler.toml — bind a Worker so a routing rule can "Send to a Worker".
name = "inbound-mail"
main = "src/worker.js"
compatibility_date = "2024-09-23"

3. Parse the message in an Email Worker

An Email Worker exports an email() handler that runs for each message. Cloudflare passes a ForwardableEmailMessage: the envelope from and to are plain strings and headers is a Headers object, but the subject, bodies, and attachments live inside message.raw — a ReadableStream of the raw MIME. There's no decoded JSON, so you parse the MIME yourself; the postal-mime library handles boundaries, encodings, and charsets:

install & deploy
npm install postal-mime
npx wrangler deploy
src/worker.js
import PostalMime from "postal-mime";

export default {
async email(message, env, ctx) {
// Cloudflare hands you a ForwardableEmailMessage. The envelope fields are
// ready — but everything else lives inside the raw MIME you must parse.
// message.from / message.to → envelope addresses (strings)
// message.headers → a Headers object (Subject, Message-ID, …)
// message.raw → a ReadableStream of the raw MIME bytes

const email = await PostalMime.parse(message.raw);

const parsed = {
from: message.from,
to: message.to,
subject: email.subject,
text: email.text,
html: email.html,
// postal-mime inlines each attachment as { filename, mimeType, content }.
attachments: email.attachments,
};

// Hand the decoded message to your app.
await fetch("https://yourapp.com/hooks/cloudflare", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(parsed),
});

// You can also act on the message in the Worker itself:
// message.setReject("Address unknown"); // bounce it
// await message.forward("support@yourdomain.com"); // deliver to a mailbox
// await message.reply(reply); // send an automatic reply
},
};

That's inbound on Cloudflare: enable Email Routing, add a rule that sends the address to a Worker, and let the Worker read message.raw, parse it with postal-mime, and POST the decoded result to your app. The same message can also setReject(), forward(), or reply() if you'd rather act at the edge.

Part 2 — The same flow on MailKite

When we built inbound into MailKite, we wanted three things to be true: one record to receive on your whole domain, a payload you don't have to reshape — already-decoded JSON at any HTTP endpoint, no Worker and no MIME parsing — and a webhook you can actually trust. Here's the same parse-a-reply flow.

1. Point your domain at MailKite

One MX record makes every address on the domain receivable — no dedicated subdomain, no second record to prioritise:

DNS — yourdomain.com
Host              Type   Priority   Value
yourdomain.com MX 10 mx.mailkite.dev

The domain verifies as soon as that record resolves. See Domains & DNS for the SPF/DKIM/DMARC records that let you send from it too.

2. Register your webhook

Add your endpoint URL in the dashboard or via the API and MailKite POSTs an email.received event the moment mail arrives. Want specific addresses to hit specific endpoints? Add a route — otherwise a catch-all sends the whole domain to one webhook.

3. Parse the payload

The message arrives already decoded — flat from, to, subject, text, and html fields, edge auth verdicts (SPF/DKIM/DMARC/spam), and attachments as signed, time-limited links rather than tokens you have to redeem:

POST /hooks/mailkite
{
"id": "msg_2Hk9…",
"type": "email.received",
"from": { "address": "ada@example.com", "name": "Ada Lovelace" },
"to": [{ "address": "support@myapp.ai", "name": "Support" }],
"subject": "Re: invoice #1042",
"text": "Looks good — approved!",
"html": "<p>Looks good — approved!</p>",
"threadId": "<a1b2c3@mail.example.com>",
"receivedAt": 1785196800000,
"receivedAtIso": "2026-07-28T00:00:00.000Z",
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "spam": "ham" },
"attachments": [
{
"id": "msg_2Hk9…:0",
"filename": "po.pdf",
"contentType": "application/pdf",
"size": 18213,
"url": "https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…"
}
]
}

Because the URL is public, MailKite signs every request. Verify the signature over the raw body first, then handle the event — one message per call, no batch to unwrap:

handle-inbound
import express from "express";
import { MailKite } from "mailkite";

const SECRET = process.env.MAILKITE_WEBHOOK_SECRET;
const app = express();
app.use("/hooks/mailkite", express.raw({ type: "application/json" }));

app.post("/hooks/mailkite", (req, res) => {
if (!MailKite.verifyWebhook(req.headers["x-mailkite-signature"], req.body, SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
// ...handle event.type === "email.received"
// Confirm receipt: returns the JSON body {"status":"ok"}.
res.type("application/json").send(MailKite.replyOk());
});
Install Docs →

The verifyWebhook helper recomputes the HMAC and rejects forged or stale events — see Verifying signatures for the header format and a no-SDK version. Fields and attachment links are documented in full under Inbound webhooks.

A little about MailKite

MailKite is programmable email for developers and AI agents. Publish one MX record and every address on your domain becomes a signed webhook of clean, decoded JSON — no IMAP, no polling, no MIME wrangling. The same API sends mail, and any inbox can be handed straight to an agent that reads and replies on its own. Inbound and receiving are on the free tier.

Next: Inbound webhooks for routing, retries, and attachments, or the Quickstart to get a verified domain and API key.