All posts
The MailerSend alternative for AI agents
Gabe 18 min read

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.

MailerSend vs MailKite
MailerSend vs MailKite — the same job (an inbox for your AI agent), two approaches.

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).

MailerSend sender MailerSendinbound route signed webhookspf_check + dkim_check your handlercompose trust + model send APIreply out No DMARC and no spam verdict in the payload, and no built-in agent loop: the trust call and the model loop are both yours to assemble. MailKite sender MX edgeparse + auth JSON webhookauth: {spf,dkim,dmarc,spam} agent + replyyours or action:agent One normalized verdict block, and the model loop is optional — MailKite can run it.
Same inbound email, two agent paths. MailerSend parses and signs; you compose the trust call and host the loop. MailKite hands you a normalized verdict and will run the loop if you want.

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.

  • Inbound Routing that actually parses. Point MX at MailerSend, add an inbound route (catch-all or a specific recipient, with match filters on sender, domain, or header), and it POSTs the message as JSON with decoded text, html, subject, from, headers, and attachments. No IMAP, no raw MIME to parse yourself.
  • Signed webhooks, done right. Each webhook gets a signing secret; MailerSend sends a Signature header that's an HMAC-SHA256 of the raw body. Their docs show crypto.timingSafeEqual for the compare. That's the correct pattern, not an afterthought.
  • Sending is first-class. Clean REST API, an SMTP relay, a real Node SDK (EmailParams, setInReplyTo, setReferences), and templates. Replying from an inbound handler is a few setters. As a send-and-receive pair for an agent, it's genuinely workable.

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.

MailerSend: compose it spf_check {code, value}read value === "pass" dkim_check (boolean)true or false DMARC alignmentnot in the payload spam / ham verdictnot in the payload your composed trustpartial, and yours to maintain MailKite: read one verdict auth: {spf, dkim, dmarc, spam}resolved at the edge, one field Same email. Left: four signals to gather, two missing. Right: one block the agent reads before it acts.
The trust decision an agent makes before acting on an email. MailerSend hands you SPF and DKIM to combine; MailKite resolves SPF, DKIM, DMARC, and spam into one 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

MailerSendMailKite
Inbound deliveryParsed JSON webhook (data.*)Parsed JSON webhook (email.received)
Auth signalsspf_check + dkim_checkauth: {spf, dkim, dmarc, spam}
DMARC / spam verdictNot in payloadIncluded, normalized
Webhook signingHMAC-SHA256 Signature headerHMAC + replay window, one-call verify
Built-in agent loopNone (webhook only)Optional route action: 'agent'
Failed-delivery replayRetriesRetries + one-click replay
Free tier500 emails/mo, 1 domain3,000 msgs/mo, no per-domain fee
SendingREST + SMTP + templatesREST + 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.

Discuss this post: Hacker News Share on X Share on LinkedIn

Related posts