# The MailerSend alternative for AI agents

> Source: <https://mailkite.dev/blog/mailersend-for-ai-agents/>
> Published: 2026-07-04 00:00:00+00:00

# 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](https://github.com/mailkite/demo-mailersend-ai-agent)).

Everything below is a repo you can run while you read. Clone [ demo-mailersend-ai-agent](https://github.com/mailkite/demo-mailersend-ai-agent) and

`npm start`

, or [open it in StackBlitz](https://stackblitz.com/github/mailkite/demo-mailersend-ai-agent?file=server.mjs)(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 [, so every contrast here is something you can diff and test, not take on faith. A domain comes in only when you want](https://github.com/mailkite/demo-mailersend-ai-agent/blob/main/mailersend-contrast/handler.mjs)

`mailersend-contrast/`

*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`

):

``` python
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](/docs/receiving) and [sending docs](/docs/sending). [Open the demo in StackBlitz](https://stackblitz.com/github/mailkite/demo-mailersend-ai-agent?file=server.mjs) 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](https://github.com/mailkite/demo-mailersend-ai-agent/blob/main/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](https://github.com/mailkite/demo-mailersend-ai-agent/blob/main/server.mjs) never composes anything.

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](/blog/agent-inbox-security-by-design/), 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](https://github.com/mailkite/demo-mailersend-ai-agent): [ server.mjs](https://github.com/mailkite/demo-mailersend-ai-agent/blob/main/server.mjs) is the bring-your-own-webhook version at the top of this post, and

[dry-runs this hosted route. MailerSend has no equivalent to the second — you assemble the model and the send call yourself — and](https://github.com/mailkite/demo-mailersend-ai-agent/blob/main/managed-route.mjs)

`managed-route.mjs`

`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](https://github.com/mailkite/demo-mailersend-ai-agent) (or [run it in your browser](https://stackblitz.com/github/mailkite/demo-mailersend-ai-agent?file=server.mjs)), then [point a domain at MailKite](/docs/quickstart) 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.*
