The MailerSend alternative for AI agents
MailerSend (by MailerLite) is a developer-friendly transactional service, and its Inbound Routing genuinely receives mail as parsed JSON. For an autonomous agent the gaps are narrower than usual: you compose the trust decision yourself from spf_check and dkim_check (no DMARC, no spam verdict), and there's no built-in agent loop. MailKite, which we build, hands you a normalized auth verdict and a receive→reply loop on a domain you own. For developers giving an agent its own inbox.
Most “does it receive?” comparisons are lopsided, but this one isn’t, so the diagram earns its place up front: MailerSend really does deliver inbound email as parsed JSON, and the difference for an agent is subtler than “webhook vs no webhook.” It’s what arrives in that JSON, and what your loop has to decide before it acts. The rest of the post is the honest version of this picture, plus the ~25 lines that are the whole MailKite side (from a runnable demo repo).
Everything below is a repo you can run while you read. Clone demo-mailersend-ai-agent and npm start, or open it in StackBlitz (real Node in a browser tab, no account) where it runs on load. You don’t need a domain to see the loop run: a webhook normally needs a public URL, so npm start boots the server and self-fires a correctly signed email.received event at its own localhost — the whole receive→verify→think→reply loop runs in one command (the reply is a dry-run, and managed-route.mjs dry-runs the hosted path too). It also ships the MailerSend path next to it in mailersend-contrast/, so every contrast here is something you can diff and test, not take on faith. A domain comes in only when you want real inbound email to arrive.
Here’s the whole MailKite side of an agent inbox: email in, verify the signature, hand the body to your model, reply through the same client. This is the heart of server.mjs in that repo — wrapped in a dry-run harness and a stub agent so it runs with no key and no LLM, but the loop is exactly this, and it runs as pasted on Node 18+ (npm install mailkite express):
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 is untrusted INPUT, never instructions. One block decides trust.
const answer = await runAgent({
task: event.text,
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, thinks, and answers, with event.auth.dmarc already resolved for the trust check. npm start boots the server and self-fires the exact payload MailKite’s delivery worker sends, so in one command you watch the agent read the task, take its trust verdict straight off event.auth, and dry-run the reply — no account or LLM required. The identical handler shape exists for Python, Ruby, Go, PHP, and Java; see the receiving docs and sending docs. Open the demo in StackBlitz to run it in your browser.
Where MailerSend wins for agents, honestly
I want to be fair here, because MailerSend’s inbound is not a bolt-on. It’s a real feature and it’s pleasant to use.
If you’re already on MailerSend for outbound and you want an agent to read replies, the honest answer is: their inbound gets you most of the way there. This post isn’t “MailerSend can’t receive.” It’s about the two places an agent path still asks you to do the work.
What MailerSend asks of an agent builder
The inbound payload nests the message under data, and the two authentication signals it gives you are spf_check (a { code, value } object) and dkim_check (a boolean). There is no DMARC result and no spam classification. So before your agent can weigh whether to trust an instruction, you compose the verdict yourself. Here’s the real MailerSend inbound handler (mailersend-contrast/handler.mjs in the demo):
// MailerSend inbound: verify Signature header, then compose the trust decision yourself
import crypto from "node:crypto";
import { MailerSend, EmailParams, Sender, Recipient } from "mailersend";
const ms = new MailerSend({ apiKey: process.env.MAILERSEND_API_KEY });
export async function handleInbound(req, res) {
const raw = req.rawBody; // keep the raw bytes for HMAC
const expected = crypto.createHmac("sha256", process.env.MS_SIGNING_SECRET)
.update(raw, "utf8").digest("hex");
const got = req.headers["signature"] ?? "";
if (!crypto.timingSafeEqual(Buffer.from(got, "hex"), Buffer.from(expected, "hex"))) {
return res.sendStatus(401);
}
const { data } = JSON.parse(raw); // message nested under data
// spf_check is {code, value} (the code set isn't fully documented, so you normalize it);
// dkim_check is a boolean. No DMARC alignment and no spam score are in the payload.
const spfPass = data.spf_check?.value === "pass";
const dkimPass = data.dkim_check === true;
const trusted = spfPass && dkimPass; // partial: no DMARC, no spam signal to weight
const answer = await runAgent({ task: data.text, from: data.from?.email, trusted });
const reply = new EmailParams()
.setFrom(new Sender(data.recipients.rcptTo, "Agent"))
.setTo([new Recipient(data.from.email)])
.setSubject(`Re: ${data.subject}`)
.setInReplyTo(data.headers["message-id"])
.setHtml(answer.html);
await ms.email.send(reply);
res.sendStatus(200);
}
Nothing here is hard. But notice what the agent is left holding: it derives trusted from two raw signals and then has to accept that DMARC alignment and a spam score simply aren’t in the payload. For a program that acts on the contents of email, that missing normalized verdict is the load-bearing gap, not the plumbing. The repo pins both costs — npm test runs composeTrust() and watches dmarc and spam come back null (a partial verdict you maintain), then runs the handler end to end to show the reply is a second call to the send API, threaded by hand. On MailKite that decision is one field, event.auth, which is the whole reason server.mjs never composes anything.
auth block.There’s a second gap, and it’s about who runs the loop. MailerSend delivers the message and stops; the model call, the retry policy, and the “did this run actually finish?” bookkeeping are your service to build and keep up. That’s a reasonable division of labor for a send-focused platform. It’s just work the agent path still owns.
MailerSend even ships an official MCP server, which is a genuinely nice touch for an agent stack. It’s worth being precise about what it does: it exposes sending, domain management, and analytics as tools, not inbox reading. So an agent can send through MailerSend’s MCP, but it can’t read its own mail through it; the inbound half is still the webhook you wire up.
The comparison, no adjective inflation
| MailerSend | MailKite | |
|---|---|---|
| Inbound delivery | Parsed JSON webhook (data.*) | Parsed JSON webhook (email.received) |
| Auth signals | spf_check + dkim_check | auth: {spf, dkim, dmarc, spam} |
| DMARC / spam verdict | Not in payload | Included, normalized |
| Webhook signing | HMAC-SHA256 Signature header | HMAC + replay window, one-call verify |
| Built-in agent loop | None (webhook only) | Optional route action: 'agent' |
| Failed-delivery replay | Retries | Retries + one-click replay |
| Free tier | 500 emails/mo, 1 domain | 3,000 msgs/mo, no per-domain fee |
| Sending | REST + SMTP + templates | REST + SMTP submission edge |
The through-line: MailerSend gives you a clean, signed, parsed inbound feed and leaves the trust verdict and the agent loop to you. MailKite resolves the verdict and will host the loop, on domains that don’t cost extra to add.
What actually hits your agent’s webhook
Here’s the MailKite email.received event. The auth block is the whole point of the second diagram: SPF, DKIM, DMARC, and a spam classification, already resolved, so the agent reads one field instead of composing a decision:
{
"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=…" }
]
}
Don’t take the shape on faith — fire one and watch it come back, auth block and all. Spoof the SPF/DKIM to fail and see the verdict flip; that’s the field the MailerSend path makes you compose from spf_check and dkim_check:
Treat the body as data, never as instructions. The auth block is what lets the agent decide whether to weight a sender before it reads a word of the message, and checking it is necessary but not sufficient: the real defense is bounding what a fooled agent can do. That’s the subject of the agent-security post, and it applies whichever provider parses your mail.
One thing MailKite does that a send-first provider doesn’t
MailKite, which we build, was designed inbound-first, so the agent loop is a first-class option rather than something you assemble. Beyond the webhook lane above, a route whose action is agent runs the model loop for you:
await mk.createRoute({
match: "agent@myapp.ai",
action: "agent",
agentPrompt: "Read incoming mail, answer billing questions from the docs, escalate the rest.",
});
The run executes on a Cloudflare Queue with its own budget, capped at a few tool rounds, reaped at five minutes so a slow model can’t wedge the pipeline, and recorded as a transcript you can drill into. Delivery failures are retried and one-click replayable from the dashboard, so a bad deploy on your side doesn’t drop an inbound message on the floor. To start, DNS-verify the domain (MX to receive, SPF + DKIM to send); there’s no sandbox or account-approval wait (MailerSend gates new accounts behind a trial-approval step that can take up to a day). The free tier is 3,000 messages a month, in and out, with no per-domain fee, so an agent can own as many scoped addresses as it needs.
Both ways to run the loop are in the demo repo: server.mjs is the bring-your-own-webhook version at the top of this post, and managed-route.mjs dry-runs this hosted route. MailerSend has no equivalent to the second — you assemble the model and the send call yourself — and npm test runs the MailKite side right next to the mailersend-contrast/handler.mjs version so you can diff the two.
FAQ
Can MailerSend receive inbound email for an agent?
Yes. MailerSend’s Inbound Routing points your MX at MailerSend, matches messages with catch-all or per-recipient filters, and POSTs the parsed email to your webhook as JSON. The message nests under data with decoded text, html, subject, from, headers, and attachments, and the webhook is HMAC-SHA256 signed. It’s a real inbound feed; the agent-specific gaps are the trust verdict and the loop, not parsing.
Does MailerSend’s inbound payload include SPF, DKIM, and DMARC?
It includes spf_check (a { code, value } object) and dkim_check (a boolean). There is no DMARC alignment result and no spam classification in the payload, so an agent composes its trust decision from the two signals it does get. MailKite delivers a single normalized auth: {spf, dkim, dmarc, spam} block.
Is MailerSend’s inbound routing free? Inbound routing is available on the free plan, but two details matter for an agent. As of December 2025 the free plan is 500 emails/month (100/day, card required) on one verified domain, and forwarded or posted inbound messages count against that same monthly quota; the former 3,000/month tier is now paid from $7/month. MailKite’s free tier is 3,000 messages a month across inbound and outbound with no per-domain fee.
How does an agent reply to an inbound email on MailerSend?
Through the send API. The Node SDK builds a reply with EmailParams, setInReplyTo, and setReferences to thread it, using the original message-id from data.headers. It works; you’re just wiring the send half yourself. On MailKite, mk.send({ inReplyTo: event.id }) threads from the event you already have.
Do I have to move my sending off MailerSend to use MailKite for an agent? No. MailKite speaks plain HTTPS and REST, and offers an SMTP submission edge, so you can run the agent’s inbox on MailKite and keep sending wherever you already do. You’re replacing the inbound trust-and-loop layer, not your provider.
MailerSend’s inbound is one of the more developer-friendly ones out there, and if you’re already sending with it, its parsed webhook gets your agent most of the way. If the missing verdict block and the do-it-yourself loop are the parts you’d rather not own, clone the demo repo (or run it in your browser), then point a domain at MailKite and your next inbound email arrives with the verdict already resolved.
Related: the pillar on giving your agent an inbox and agent inbox security by design.