The Resend alternative for AI agents
Resend has first-class sending DX and, since November 2025, real inbound email. But its inbound webhook is metadata only: an agent fetches the body with a second API call and derives SPF/DKIM trust itself. MailKite (which we build) pushes the parsed body and an auth verdict in one event, so an agent's receive→reply loop is a single round trip. For developers giving an autonomous agent its own inbox.
Concretely: on Resend, email.received fires with the sender, subject, and attachment stubs, and then you call resend.emails.receiving.get(email_id) to pull the actual text and html. Two hops before your model reads a word, and the SPF/DKIM/DMARC result an agent needs to decide whether to trust the message isn’t in the event at all. For a human-facing app that’s a shrug. For an autonomous agent running a receive→think→reply loop, every extra hop and every “derive it yourself” is a place the loop can stall or misjudge a spoofed sender. Here’s that gap in one picture, then the whole MailKite side in about 25 lines.
Here’s the bring-your-own-agent loop, whole. Email in, verify the signature, hand the parsed body to your model, reply through the same client. It runs as pasted on Node 18+ (npm install mailkite express), and the full version lives in a runnable demo repo:
import express from "express";
import { MailKite } from "mailkite";
const app = express();
const mk = new MailKite(process.env.MAILKITE_API_KEY);
const SECRET = process.env.MAILKITE_WEBHOOK_SECRET;
app.use("/hooks/agent", express.raw({ type: "application/json" }));
app.post("/hooks/agent", async (req, res) => {
// signature check, replay window, constant-time compare — one call
if (!MailKite.verifyWebhook(req.headers["x-mailkite-signature"], req.body, SECRET)) {
return res.sendStatus(401);
}
res.sendStatus(200); // ack fast; run the agent out of band
const event = JSON.parse(req.body);
if (event.type !== "email.received") return;
// the body and the auth verdict are already in the event — no second fetch
const answer = await runAgent({
task: event.text, // parsed body, right here
from: event.from.address,
trusted: event.auth.spf === "pass" && event.auth.dmarc === "pass",
});
await mk.send({
from: event.to[0].address, // reply from the address it was sent to
to: event.from.address,
subject: `Re: ${event.subject}`,
inReplyTo: event.id, // threads the reply
html: answer.html,
});
});
app.listen(3000);
That’s a fully autonomous email agent: it hears, it thinks, it answers, with the body and the SPF/DKIM/DMARC result both in the event that woke it up. mk.send() returns { id, status } so you can log the outbound message, and the identical handler shape exists for Python, Ruby, Go, PHP, and Java; see the receiving docs and sending docs. Open it in StackBlitz to run real Node in a browser tab.
Where Resend wins for agents, honestly
Resend has the best sending developer experience in this category, and I’m not going to pretend otherwise. If your agent’s job is mostly to send, Resend is a genuinely great pick:
This post isn’t “Resend is bad.” It’s “Resend was built send-first, and its inbound half asks the agent builder to do the assembly.”
What Resend asks of an agent builder
Resend’s inbound is real and shipped, but the shape matters for an agent. The webhook is metadata only. Resend’s own docs say it plainly: “Webhooks do not include the email body, headers, or attachments, only their metadata.” So the receive step is a verify, then a fetch, then your own trust check, in Resend’s idiom:
// Resend inbound: verify the Svix signature, then FETCH the body (it isn't in the webhook)
import { Resend } from "resend";
import { Webhook } from "svix"; // Resend signs inbound webhooks with Svix
const resend = new Resend(process.env.RESEND_API_KEY);
const wh = new Webhook(process.env.RESEND_WEBHOOK_SECRET);
app.post("/hooks/inbound", async (req, res) => {
const evt = wh.verify(req.rawBody, {
"svix-id": req.headers["svix-id"],
"svix-timestamp": req.headers["svix-timestamp"],
"svix-signature": req.headers["svix-signature"],
});
res.sendStatus(200);
if (evt.type !== "email.received") return;
// the event carries metadata: from, to, subject, attachment stubs — no body.
// pull the stored message to get text/html:
const { data: mail } = await resend.emails.receiving.get(evt.data.email_id);
// and there's no SPF/DKIM/DMARC verdict in the payload — you derive trust yourself
await runAgent({ task: mail.text, from: evt.data.from });
});
Nothing here is hard. But notice what the agent path accumulates: a second network call before the model sees the body, an attachment fetch after that if the message has one, and a trust decision you make from raw headers because the event doesn’t hand you an SPF/DKIM/DMARC result. Stack the inbound steps up and the assembly is visible:
The comparison, no adjective inflation
| Resend | MailKite | |
|---|---|---|
| Inbound email | Yes, shipped Nov 2025 | Yes |
| Body in the webhook | Metadata only; fetch via API | Parsed text + html in the event |
| SPF/DKIM/DMARC in payload | Not in the inbound event | auth block inline |
| Round trips to read one email | Webhook + a GET | One pushed event |
| Reply threading | Send API | inReplyTo, returns { id, status } |
| Sandbox / approval | None, instant production | None, DNS-verify then send |
| Domains on free tier | 1 | No per-domain fee |
| Free tier | 3,000/mo, 100/day cap | 3,000/mo (in + out) |
| Run the agent loop for you | You host it (MCP for tools) | Route action: 'agent' on a queue |
| React templating | React Email (they maintain it) | Bring your own / templates |
The through-line: Resend wins send-side DX, React Email, and instant production, and if you’re sending-first it’s a great tool. MailKite wins the receive side an agent leans on: the parsed body and a trust verdict arrive in one event, so the loop is a single round trip. Two of those rows (a fleet of agents each on its own domain hits Resend’s one-domain free tier; MailKite has no per-domain fee) also matter once you run more than one agent.
What actually hits your agent’s webhook
The same inbound email, delivered parsed, with the trust result already resolved. No second fetch, no header parsing:
{
"id": "msg_2Hk9…",
"type": "email.received",
"from": { "address": "ada@example.com" },
"to": [{ "address": "agent@myapp.ai" }],
"subject": "Re: invoice #1042",
"text": "Looks good — approved!",
"html": "<p>Looks good — approved!</p>",
"threadId": "<a1b2c3@mail.example.com>",
"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=…" }
]
}
That auth block is load-bearing for an agent. From: is plain text and trivially spoofable, so the email body is untrusted input, never instructions; the moment your agent follows what a message says, that body is a prompt-injection vector. Checking auth before you weight a sender’s instructions is necessary but not sufficient. The real fix is architectural, and it’s the whole point of agent inbox security by design: read that before either loop acts on anything that matters.
One more option: let MailKite run the agent
Both paths above have you hosting the model loop. On MailKite you can also make the route itself the agent. A route with action: "agent" carries an agentPrompt, and MailKite, which we build, runs the model loop for you on a durable Cloudflare Queue with its own execution budget, a per-run tool-round cap, a five-minute reaper, and a full transcript you can drill into per route:
await mk.createRoute({
match: "support@myapp.ai",
action: "agent",
agentPrompt: "Answer billing questions from the docs. Escalate anything else to humans@myapp.ai.",
});
The system prompt bakes in the safety rules: the body is untrusted, at most one reply, never reply to no-reply senders. Bring-your-own wins when the agent’s brain is your code; the built-in route wins when the job is “read this inbound mail, answer or escalate” and you’d rather not run the infrastructure.
FAQ
Does Resend support inbound email?
Yes, since November 2025. An email.received webhook fires when mail arrives at your Resend-managed or MX-configured domain. The important detail for an agent: the webhook carries metadata only (sender, subject, attachment stubs). To read the body you call the received-emails API with the email_id. MailKite delivers the parsed body in the webhook itself.
What’s the actual difference for an AI agent?
Round trips and trust. On Resend the agent gets a metadata webhook, fetches the body on a second call, and derives SPF/DKIM/DMARC itself. On MailKite the parsed text, html, a resolved threadId, and an auth verdict arrive in one signed event, so the receive step is a single hop and the trust signal is already there.
Is Resend a good choice for sending from an agent? Yes. Resend’s send API and React Email are excellent, production access is immediate with no sandbox, and there’s an official MCP server. If your agent is send-first, Resend is a strong pick. The gap this post is about is the inbound half of an autonomous loop.
Can I keep using React Email with MailKite?
Yes. React Email compiles to plain HTML that works with any provider, so render your components to a string and pass it as html to mk.send(). You’re not locked out of Resend’s authoring tools by receiving on MailKite.
Do I have to wait for approval to start on MailKite? No, same as Resend on this point. Verify your domain over DNS (SPF + DKIM to send, MX to receive) and you can send to anyone. There’s no sandbox and no per-domain fee; the free tier is 3,000 messages a month across inbound and outbound. See the quickstart.
If Resend has your agent fetching every inbound body on a second call and reconstructing SPF/DKIM by hand, there’s a shorter loop. Clone the demo repo (or run it in your browser), then point a domain at MailKite and your agent’s next inbound email arrives parsed, authenticated, and ready to answer.
Related: the pillar on giving your agent an inbox, agent inbox security by design, and the full MailKite vs Resend comparison.