MailKite
Get your API key
All guides Inbound email parsing

Resend inbound email

Receiving inbound email turns the messages 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 receiving email on Resend end to end, then shows the same flow on MailKite.

What you'll need

  • A domain you control, with access to its DNS records.
  • A public HTTPS endpoint to receive the webhook (a tunnel like ngrok works while testing).
  • A Resend account and API key for the first half.

Part 1 — Receive inbound email with Resend

Inbound is a newer part of Resend, so treat its exact record values and API surface as whatever the receiving docs show at setup time — the details below are current as of this writing.

1. Point your domain at Resend

Add an MX record so mail for the domain is delivered to Resend. Resend shows the exact value to use when you enable receiving on a domain; the record must have the lowest priority so mail routes to Resend rather than another server:

DNS — yourdomain.com
Host              Type   Priority   Value
yourdomain.com MX 10 <shown in your Resend dashboard>

Once the record resolves, any address at that domain — support@yourdomain.com, ada@yourdomain.com — will flow into Resend. (You can also test against a .resend.app address without touching DNS.)

2. Subscribe to the inbound event

Resend delivers inbound mail over its webhook system, which is signed with Svix. In the dashboard, add a webhook endpoint and subscribe it to the email.received event. Resend then signs and POSTs to your URL every time a message arrives, including the headers svix-id, svix-timestamp, and svix-signature.

3. Handle the event and fetch the body

One thing to know up front: the email.received webhook carries metadata onlyemail_id, from, to/cc/bcc, subject, and attachment descriptors. It does not include the message body, headers, or attachment contents. You retrieve those with a follow-up call to the Received Emails API:

POST /hooks/resend
{
"type": "email.received",
"created_at": "2024-02-22T23:41:12.126Z",
"data": {
"email_id": "a1b2c3d4-…",
"from": "ada@example.com",
"to": ["support@yourdomain.com"],
"cc": [],
"bcc": [],
"subject": "Can't update my card",
"attachments": [
{
"id": "att_abc123",
"filename": "screenshot.png",
"content_type": "image/png"
}
]
}
}

Verify the Svix signature over the raw body first — the Resend SDK's webhooks.verify() helper does this using the three svix-* headers and your webhook signing secret (you can also use the standalone svix library). Then read the metadata and fetch the full message:

handle-inbound.js
import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

export async function POST(req) {
const payload = await req.text(); // raw body — Svix signs the bytes

const event = resend.webhooks.verify({
payload,
headers: {
"svix-id": req.headers.get("svix-id"),
"svix-timestamp": req.headers.get("svix-timestamp"),
"svix-signature": req.headers.get("svix-signature"),
},
secret: process.env.RESEND_WEBHOOK_SECRET,
});

if (event.type === "email.received") {
const { email_id, from, subject } = event.data;
console.log("from", from, "·", subject);

// The webhook carries metadata only — no body, headers, or attachment
// content. Fetch the full message with a follow-up API call:
const full = await resend.emails.receiving.get(email_id);
const body = full.data?.text ?? full.data?.html;
// ...open a ticket, reply, hand to an agent.
}

return new Response("OK", { status: 200 });
}

That's receiving on Resend: an MX record at the lowest priority, a Svix-signed email.received webhook, and a second API call per message to pull the body and attachments.

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 fetch twice, and a webhook you can actually trust. Here's the same receive-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 IDs you have to redeem with a second call:

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, everything you need already in the payload:

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.