{"slug": "the-mailchimp-transactional-mandrill-alternative-for-ai-agents", "title": "The Mailchimp Transactional (Mandrill) alternative for AI agents", "summary": "MailKite launches a Mailchimp Transactional (Mandrill) alternative designed for AI agents, offering a streamlined receive→reply loop with built-in auth verification and parsed JSON events, while Mandrill requires a paid Mailchimp marketing plan and lacks native agent integration.", "body_md": "# The Mailchimp Transactional (Mandrill) alternative for AI agents\n\nMandrill can receive email, but it's gated behind a paid Mailchimp marketing plan and hands you inbound as a batched mandrill_events array with no auth verdict and no agent loop. MailKite (which we build) gives an agent a real scoped inbox delivered as one parsed JSON event with a receive→reply loop. For developers wiring an autonomous agent to an inbox.\n\nSo the first surprise isn’t technical: to point an agent at a Mandrill inbox you first need a paid Mailchimp *marketing* account (Standard or Premium), and then Transactional as an add-on on top. Mandrill hasn’t been standalone since 2016. Everything below is the honest version of what that buys an agent builder, where Mandrill genuinely holds up, and the ~20 lines that are the entire MailKite side.\n\nEverything below is a repo you can run while you read. Clone [ demo-mandrill-ai-agent](https://github.com/mailkite/demo-mandrill-ai-agent) and\n\n`npm start`\n\n, or [open it in StackBlitz](https://stackblitz.com/github/mailkite/demo-mandrill-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\n\n`npm start`\n\nboots the server and self-fires a correctly *signed*\n\n`email.received`\n\nevent 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`\n\ndry-runs the hosted path too). It also ships the Mandrill path next to it in `mandrill-contrast/`\n\n, 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.\n\nHere’s the whole MailKite side: email in, verify the signature, hand the body to your model, reply through the same client. This is the heart of `server.mjs`\n\nin that repo — the repo wraps it 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`\n\n):\n\n``` python\nimport express from \"express\";\nimport { MailKite } from \"mailkite\";\n\nconst app = express();\nconst mk = new MailKite(process.env.MAILKITE_API_KEY);\nconst SECRET = process.env.MAILKITE_WEBHOOK_SECRET;\n\napp.use(\"/hooks/agent\", express.raw({ type: \"application/json\" }));\n\napp.post(\"/hooks/agent\", async (req, res) => {\n  // signature check, replay window, constant-time compare — one call\n  if (!MailKite.verifyWebhook(req.headers[\"x-mailkite-signature\"], req.body, SECRET)) {\n    return res.sendStatus(401);\n  }\n  res.sendStatus(200);\n\n  const event = JSON.parse(req.body);\n  if (event.type !== \"email.received\") return;\n\n  // Body is untrusted INPUT, never instructions. Use auth to decide trust.\n  const answer = await runAgent({\n    task: event.text,\n    from: event.from.address,\n    trusted: event.auth.spf === \"pass\" && event.auth.dmarc === \"pass\",\n  });\n\n  await mk.send({\n    from: event.to[0].address,   // reply from the address it was sent to\n    to: event.from.address,\n    subject: `Re: ${event.subject}`,\n    inReplyTo: event.id,         // threads the reply\n    html: answer.html,\n  });\n});\n\napp.listen(3000);\n```\n\nThat’s the receive→think→reply loop, whole. `mk.send()`\n\nreturns `{ id, status }`\n\n, and `npm start`\n\nboots 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`\n\n, and dry-run the reply — no account or LLM required. The same handler shape exists for Python, Ruby, Go, PHP, and Java: see the [receiving docs](/docs/receiving) and [sending docs](/docs/sending). The rest of this post is the honest version of the diagram: where Mandrill genuinely fits an agent, what its inbound path asks you to build, and what the parsed payload actually contains.\n\n## Where Mandrill wins for agents, honestly\n\nMandrill is not a toy, and if you’re already living in Mailchimp it can be the pragmatic call:\n\nI won’t pretend Mandrill can’t receive email. It can, and has for years. The friction for an *agent* is different: what it costs to get in the door, and how much unpacking sits between “an email arrived” and “my model can act on it.”\n\n## What Mandrill asks of an agent builder\n\nTwo things, before any model runs.\n\nFirst, the account. Transactional is an add-on to a paid Mailchimp Standard ($20/mo) or Premium plan; Free and Essentials plans can’t enable it. Transactional itself is block-based: a block is 25,000 emails starting at ~$20, cheaper per block at volume. So an agent that just needs to read a few verification emails still starts around $40/month of marketing-suite spend before it sends its first reply.\n\nSecond, the inbound shape. Mandrill doesn’t hand you one clean message. It POSTs a form-encoded body whose `mandrill_events`\n\nfield is a JSON *string* holding an array of events, and you verify the `X-Mandrill-Signature`\n\nby rebuilding the signed string yourself: the exact webhook URL, then every POST key and value concatenated in sorted key order, HMAC-SHA1’d with your webhook key, base64-encoded. Here’s that handler, honestly, in Mandrill’s own idiom — it’s [ mandrill-contrast/handler.mjs](https://github.com/mailkite/demo-mandrill-ai-agent/blob/main/mandrill-contrast/handler.mjs) in the same repo, sitting right next to the MailKite\n\n`server.mjs`\n\nabove so you can put them side by side:\n\n```\n// Mandrill inbound: form-encoded POST, mandrill_events is a JSON string you unpack\nimport express from \"express\";\nimport crypto from \"node:crypto\";\n\nconst app = express();\napp.use(express.urlencoded({ extended: false }));\n\nconst WEBHOOK_KEY = process.env.MANDRILL_WEBHOOK_KEY;\nconst WEBHOOK_URL = \"https://myapp.ai/hooks/mandrill\"; // must match what you registered, exactly\n\nfunction verify(body, signature) {\n  let signed = WEBHOOK_URL;\n  for (const key of Object.keys(body).sort()) signed += key + body[key]; // sorted key+value, no delimiter\n  const expected = crypto.createHmac(\"sha1\", WEBHOOK_KEY).update(signed).digest(\"base64\");\n  return expected === signature; // Mandrill sends base64 of the BINARY hmac, not hex\n}\n\napp.post(\"/hooks/mandrill\", (req, res) => {\n  if (!verify(req.body, req.headers[\"x-mandrill-signature\"])) return res.sendStatus(401);\n\n  const events = JSON.parse(req.body.mandrill_events); // a JSON array inside a form field\n  for (const ev of events) {\n    if (ev.event !== \"inbound\") continue;\n    const m = ev.msg;\n    // assemble your own trust verdict from separate fields\n    const trusted = m.spf?.result === \"pass\" && m.dkim?.signed && m.dkim?.valid;\n    runAgent({ task: m.text, from: m.from_email, trusted });\n    // …then a SEPARATE Messages/send API call to reply. No inReplyTo helper; you set headers.\n  }\n  res.sendStatus(200);\n});\n\napp.listen(3000);\n```\n\nNone of this is hard. But it’s a batch you iterate, a signature you rebuild from sorted POST pairs, and a trust verdict you compose by hand, all before the interesting part. Then replying is a second, unrelated API call where threading is on you. The repo pins both costs so they aren’t just claims — `npm test`\n\nshows inbound arriving as a batch array you iterate, and `trustFromResults()`\n\nreturning no DMARC verdict at all, the field the MailKite path reads straight off `event.auth`\n\n.\n\n## The comparison, for an agent\n\n| Mandrill (Mailchimp Transactional) | MailKite | |\n|---|---|---|\n| To start | Paid Mailchimp Standard+ plan, then Transactional add-on | DNS-verify a domain (SPF+DKIM to send, MX to receive) |\n| Pricing floor | ~$20/mo plan + ~$20 per 25k-email block | Free tier: 3,000 messages/mo (in + out), no per-domain fee |\n| Inbound shape | `mandrill_events[]` array in a form-encoded POST | One parsed `email.received` JSON event |\n| Auth verdict | Separate `spf` /`dkim` /`spam_report` you combine | One `auth` block: `{ spf, dkim, dmarc, spam }` |\n| Signature check | Rebuild HMAC-SHA1 over sorted POST pairs yourself | `verifyWebhook(sig, raw, secret)` — one call |\n| Reply / threading | Separate send call; you set threading headers | `mk.send({ inReplyTo: event.id })` threads it |\n| Agent loop | None; you host the whole model loop | BYO loop, or a route with `action: 'agent'` |\n\nThe through-line: Mandrill *receives*, but every convenience an agent wants (a single message, a ready trust verdict, a threaded reply, a place to run the loop) is left for you to assemble, on top of a paid marketing plan you may not otherwise want.\n\n## What actually hits your agent’s webhook\n\nOn MailKite the same inbound email arrives already decoded, one event, no array to unpack and no signature to rebuild by hand:\n\n```\n{\n  \"id\": \"msg_2Hk9…\",\n  \"type\": \"email.received\",\n  \"from\": { \"address\": \"ada@example.com\" },\n  \"to\": [{ \"address\": \"agent@myapp.ai\" }],\n  \"subject\": \"Re: invoice #1042\",\n  \"text\": \"Looks good — approved!\",\n  \"html\": \"<p>Looks good — approved!</p>\",\n  \"threadId\": \"<a1b2c3@mail.example.com>\",\n  \"auth\": { \"spf\": \"pass\", \"dkim\": \"pass\", \"dmarc\": \"pass\", \"spam\": \"ham\" },\n  \"attachments\": [\n    { \"id\": \"msg_2Hk9…:0\", \"filename\": \"po.pdf\", \"contentType\": \"application/pdf\",\n      \"size\": 18213, \"url\": \"https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…\" }\n  ]\n}\n```\n\nDon’t take the shape on faith — fire one and watch it come back, `auth`\n\nblock and all. Spoof the SPF/DKIM to `fail`\n\nand see the verdict flip; that’s the field the Mandrill path makes you assemble from separate result strings, with no DMARC to read at all:\n\nThat `auth`\n\nblock is load-bearing for an agent. The email body is untrusted input, never instructions: `From:`\n\nis trivially forged, so a naive loop that *obeys* what an email says is a prompt-injection target. Check `auth`\n\nbefore you weight a sender, and bound what a fooled agent can even do. The full argument is in [agent inbox security by design](/blog/agent-inbox-security-by-design/); read it before you point any loop at real mail.\n\n## Don’t want to host the loop?\n\nMailKite, which we build, can run the agent for you. Give a route an `action`\n\nof `agent`\n\nand an `agentPrompt`\n\n, and every matching inbound message runs the model loop on a durable queue, capped and recorded as a transcript you can drill into:\n\n```\nawait mk.createRoute({\n  match: \"support@myapp.ai\",\n  action: \"agent\",\n  agentPrompt: \"Answer billing questions from the docs. Escalate anything else to humans@myapp.ai.\",\n});\n```\n\nSame parsed inbound edge either way, and both are in the repo: bring-your-own is [ server.mjs](https://github.com/mailkite/demo-mandrill-ai-agent/blob/main/server.mjs) (the code at the top), and the hosted path is\n\n[, which dry-runs the route creation with no key. Bring-your-own wins when the agent’s brain is your code; the built-in route wins when the job is “read this mail, answer or escalate” and you’d rather not run infrastructure. Either way,](https://github.com/mailkite/demo-mandrill-ai-agent/blob/main/managed-route.mjs)\n\n`managed-route.mjs`\n\n`mandrill-contrast/handler.mjs`\n\nis the same job the Mandrill way, and `npm test`\n\nruns both sides so you can diff the shapes.## FAQ\n\n**Can Mandrill (Mailchimp Transactional) receive inbound email?**\nYes. You add an inbound domain, point your MX records at Mailchimp Transactional’s servers, and create mailbox routes that POST matching mail to your webhook as a `mandrill_events`\n\narray. It receives fine; the work is unpacking the batched array, verifying the `X-Mandrill-Signature`\n\n, and composing a trust verdict from separate `spf`\n\n/`dkim`\n\n/`spam_report`\n\nfields yourself.\n\n**Do I need a paid Mailchimp account to use Mandrill?**\nYes. Since 2016 Mandrill isn’t standalone: it’s an add-on that requires a paid Mailchimp Standard or Premium marketing plan, then transactional blocks (25,000 emails per block) on top. MailKite has no marketing-plan prerequisite and a free tier of 3,000 messages a month, inbound and outbound.\n\n**How is the Mandrill inbound webhook signed?**\nWith an `X-Mandrill-Signature`\n\nheader: base64 of an HMAC-SHA1, keyed by your webhook’s auth key, over your exact webhook URL followed by every POST key and value concatenated in sorted key order. It must be the binary HMAC base64-encoded, not hex. MailKite verifies its signature in one call: `MailKite.verifyWebhook(sig, rawBody, secret)`\n\n.\n\n**Can an agent read verification codes and reply on its own with MailKite?**\nYes. Inbound arrives as parsed JSON, your handler passes `event.text`\n\nto your model, and the agent replies with `mk.send({ inReplyTo: event.id })`\n\nfrom the address it was written to. No IMAP polling, no shared personal Gmail, no OAuth token refresh.\n\n**Is MailKite cheaper than Mandrill for an agent?**\nFor a low-volume agent, almost always: MailKite starts free (3,000 messages/mo, in + out) with no per-domain fee, while Mandrill needs a paid Mailchimp plan (~~$20/mo) plus at least one transactional block (~~$20). At very high transactional send volume Mandrill’s block pricing gets competitive per message; the trade is the marketing plan and the inbound assembly you own.\n\nIf Mandrill has you standing up a marketing plan and unpacking a `mandrill_events`\n\narray just so an agent can read one email, there’s a shorter path. Clone the [demo repo](https://github.com/mailkite/demo-mandrill-ai-agent) (or [run it in your browser](https://stackblitz.com/github/mailkite/demo-mandrill-ai-agent?file=server.mjs)), then [point a domain at MailKite](/docs/quickstart) and your agent’s next inbound email arrives as one parsed event with an auth verdict attached.\n\n*Related: the pillar on giving your agent its own inbox and agent inbox security by design.*", "url": "https://wpnews.pro/news/the-mailchimp-transactional-mandrill-alternative-for-ai-agents", "canonical_source": "https://mailkite.dev/blog/mandrill-for-ai-agents/", "published_at": "2026-07-04 00:00:00+00:00", "updated_at": "2026-07-07 01:42:56.714305+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["Mailchimp", "Mandrill", "MailKite", "StackBlitz", "Node.js", "Express"], "alternates": {"html": "https://wpnews.pro/news/the-mailchimp-transactional-mandrill-alternative-for-ai-agents", "markdown": "https://wpnews.pro/news/the-mailchimp-transactional-mandrill-alternative-for-ai-agents.md", "text": "https://wpnews.pro/news/the-mailchimp-transactional-mandrill-alternative-for-ai-agents.txt", "jsonld": "https://wpnews.pro/news/the-mailchimp-transactional-mandrill-alternative-for-ai-agents.jsonld"}}