{"slug": "the-mailgun-alternative-for-ai-agents", "title": "The Mailgun alternative for AI agents", "summary": "MailKite launches an email API designed for AI agents, offering parsed JSON inboxes and a receive-reply loop as an alternative to Mailgun's rule-engine-based inbound handling. The service targets developers building autonomous email agents, providing a simpler integration with signature verification and structured data. MailKite's approach contrasts with Mailgun's requirement for custom filter expressions and form-encoded message parsing.", "body_md": "# The Mailgun alternative for AI agents\n\nMailgun does receive inbound, but through Routes — a filter-expression rule engine you author and maintain — and the message lands as application/x-www-form-urlencoded fields you decode and verify yourself, not clean JSON. MailKite (which we build) gives an agent a real scoped inbox that arrives as parsed JSON with a receive→reply loop. For developers wiring an autonomous agent to email.\n\nHere is the difference an agent feels, in one picture: the same inbound email, and everything the agent (or you, on its behalf) has to operate to read it on each side. Mailgun genuinely receives mail. But to give an agent an inbox on it you first author a rule engine in a filter DSL, and the message still arrives form-encoded, so the agent’s “read my mail” step is a decode-and-verify chore before a single model token runs. The rest of this post is the honest version of that diagram — where Mailgun wins for agents, what it asks you to build, and the ~20 lines that are the whole MailKite side.\n\nHere is the whole MailKite side: the bring-your-own-agent loop. Email in, verify the signature, hand the body to your model, reply through the same client. It runs as pasted on Node 18+ (`npm install mailkite express`\n\n), and it’s the entire integration from the [runnable demo repo](https://github.com/mailkite/demo-mailgun-ai-agent).\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); // ack fast; run the agent out of band\n\n  const event = JSON.parse(req.body); // already parsed email, not form fields\n  if (event.type !== \"email.received\") return;\n\n  // Treat the body as untrusted INPUT, never as instructions (see below).\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 hit\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 a fully autonomous email agent: it hears, it thinks, it answers. The identical handler shape exists for Python, Ruby, Go, PHP, and Java; see the [receiving docs](/docs/receiving) and [sending docs](/docs/sending). [Open it in StackBlitz](https://stackblitz.com/github/mailkite/demo-mailgun-ai-agent?file=server.mjs) to run real Node in a browser tab and watch a signed sample event land.\n\n## Where Mailgun wins for agents, honestly\n\nThis isn’t “Mailgun is bad.” Mailgun has been doing developer email for over a decade, and a few of its properties are genuinely nice for automation:\n\nIf you’re already on Mailgun for outbound and just want to bolt on a receive path, Routes is a reasonable place to do it. The rest of this post is about what that path costs an agent builder.\n\n## What Mailgun asks of an agent builder\n\nTwo things, and both land before your model does any work. First, inbound isn’t a setting — it’s a **rule engine you author and maintain**: a Route is a filter expression paired with an action. Second, when a route forwards to a URL, Mailgun POSTs `application/x-www-form-urlencoded`\n\n(and `multipart/form-data`\n\nwhen there are attachments, which is a *different* parser you also have to handle). The parsed fields use hyphenated keys like `body-plain`\n\nand `stripped-text`\n\n, the signature arrives in the body rather than a header, and there’s no normalized SPF/DKIM/DMARC verdict — you read auth results out of the raw `message-headers`\n\nyourself.\n\nHere’s that honestly, in Mailgun’s own idiom ([ mailgun-contrast/server.mjs](https://github.com/mailkite/demo-mailgun-ai-agent/blob/main/mailgun-contrast/server.mjs) in the demo repo):\n\n```\n// 1. Author the rule engine — a Route is a filter DSL + an action.\nawait mg.routes.create({\n  expression: 'match_recipient(\"agent.*@mg.myapp.ai\")',\n  action: ['forward(\"https://myapp.ai/hooks/mailgun\")', \"stop()\"],\n  priority: 10,\n});\n\n// 2. Receive it. Mailgun POSTs application/x-www-form-urlencoded\n//    (multipart/form-data when there are attachments — a different parser).\nimport crypto from \"node:crypto\";\n\napp.post(\"/hooks/mailgun\", express.urlencoded({ extended: false }), (req, res) => {\n  const { timestamp, token, signature } = req.body;\n\n  // verify: HMAC-SHA256 over timestamp+token, signing key, constant-time\n  const digest = crypto.createHmac(\"sha256\", process.env.MG_SIGNING_KEY)\n    .update(timestamp + token).digest(\"hex\");\n  const ok = crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signature));\n  if (!ok) return res.sendStatus(401);        // a replay cache is still yours to add\n\n  const from = req.body.sender;\n  const text = req.body[\"body-plain\"];         // hyphenated cousins: stripped-text,\n  const headers = JSON.parse(req.body[\"message-headers\"] || \"[]\");  // message-headers…\n  // SPF/DKIM aren't a verdict block — you find them in `headers` yourself,\n  // then run your agent on `text`, then POST /messages to reply. That's a\n  // separate Send API call, and threading is yours to set with In-Reply-To.\n  res.sendStatus(200);\n});\n```\n\nNone of this is exotic. But it’s a rule engine, a content-type branch, a hand-rolled HMAC (with the replay cache still a TODO), hyphenated field access, and header-spelunking for auth — all sitting between an inbound email and the first line of agent logic. Stage by stage:\n\n## The comparison, no adjective inflation\n\n| Mailgun | MailKite | |\n|---|---|---|\n| Give an agent an address | Author a Route (filter DSL + action) | Set a webhook on the address |\n| Inbound payload | `x-www-form-urlencoded` / `multipart` fields | One parsed JSON webhook |\n| Field access | Hyphenated keys (`body-plain` , `stripped-text` ) | Typed `event.text` , `event.html` |\n| Signature | In the body; you HMAC timestamp+token | One `verifyWebhook()` call |\n| Replay protection | Cache the token yourself | Built into `verifyWebhook()` |\n| Auth results | Dig SPF/DKIM out of raw headers | Decoded `auth` verdict block |\n| Reply / threading | Separate Send call; set `In-Reply-To` yourself | `mk.send({ inReplyTo: event.id })` |\n| Run the agent for you | No | Route with `action: \"agent\"` |\n| Inbound routes limit | Plan-gated (e.g. 5 on Basic) | Webhooks on any verified address |\n\nThe through-line: Mailgun’s Routes are flexible and its sending is mature, but the inbound half hands an agent form fields, a hand-rolled verify, and no auth verdict. MailKite hands it JSON, one verify call, and a decoded `auth`\n\nblock — the fields an agent actually branches on.\n\n## What actually hits your agent’s webhook\n\nThe same inbound email, delivered parsed. No form decode, no content-type branch, and the `auth`\n\nblock means the agent never re-derives SPF/DKIM/DMARC from raw headers:\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\nThat `auth`\n\nblock is load-bearing for agents. The email body is **untrusted input**, never instructions: `From:`\n\nis plain text, so anyone can forge a sender and then tell your agent what to do. Check `auth`\n\nbefore you weight a sender, and remember that checking it is necessary, not sufficient — bound what a fooled agent can actually do. The reasoning is in [agent inbox security by design](/blog/agent-inbox-security-by-design/); read it before you point any loop at anything that matters.\n\nIf you’d rather not host the loop at all, MailKite, which we build, can run the agent for you: a route whose `action`\n\nis `\"agent\"`\n\ncarries a free-text `agentPrompt`\n\n, and each inbound message runs the model loop on a durable queue with a recorded transcript, a tool-round cap, and a timeout reaper. Same parsed inbound edge; the difference is only where the agent’s turns execute. Details in the [inbox-agents docs](/docs/receiving).\n\n## FAQ\n\n**Can Mailgun receive inbound email for an agent?**\nYes. You create a Route whose filter (for example `match_recipient(...)`\n\n) matches the address and whose action `forward()`\n\ns to your URL or `store()`\n\ns the message. Mailgun POSTs a parsed version of the email as `application/x-www-form-urlencoded`\n\n(or `multipart/form-data`\n\nwith attachments). MailKite delivers the same message as JSON with an `auth`\n\nblock, and you set a webhook instead of authoring a rule engine.\n\n**Why does the payload shape matter for an AI agent?**\nBecause the first thing an agent has to do is read the mail. On Mailgun that’s a decode-and-verify step — branch on content-type, parse hyphenated form keys, HMAC timestamp+token, add your own replay cache, and dig SPF/DKIM out of raw headers — all before the model runs. On MailKite the handler reads `event.text`\n\nand `event.auth`\n\ndirectly.\n\n**How do I verify a Mailgun inbound request?**\nConcatenate the `timestamp`\n\nand `token`\n\nfields (no separator), compute HMAC-SHA256 with your Webhook Signing Key, and compare the hex digest to the `signature`\n\nfield with a constant-time compare. The signature is in the body, not a header, and replay protection (caching the token) is yours to add. MailKite folds all of that into one `verifyWebhook()`\n\ncall.\n\n**Is Mailgun’s Routes DSL ever the better choice?**\nWhen you need regex fan-out across many scoped addresses or a `catch_all()`\n\n, or you’re already on Mailgun for sending and want inbound in the same account, Routes is reasonable. The cost is that you author and maintain that rule engine and still decode form-encoded payloads on your side.\n\n**Do I have to leave Mailgun to use MailKite?**\nNo. MailKite is a plain HTTPS webhook and REST API — call it from wherever your agent runs. You’re replacing the inbound Routes-and-form-decode path (and getting a decoded `auth`\n\nblock), not your infrastructure. To start, DNS-verify a domain (MX to receive, SPF + DKIM to send); there’s no sandbox approval wait, and the free tier is 3,000 messages a month, in and out.\n\nIf Mailgun has your agent authoring Routes and decoding form fields just to read one email, there’s a simpler shape. Clone the [demo repo](https://github.com/mailkite/demo-mailgun-ai-agent) (or [run it in your browser](https://stackblitz.com/github/mailkite/demo-mailgun-ai-agent?file=server.mjs)), then [point a domain at MailKite](/docs/quickstart) and your next inbound email reaches the agent as parsed JSON.\n\n*Related: the pillar on giving your AI agent its own inbox, agent inbox security by design, the full MailKite vs Mailgun Routes comparison, and the for-developers post the Mailgun Routes alternative.*", "url": "https://wpnews.pro/news/the-mailgun-alternative-for-ai-agents", "canonical_source": "https://mailkite.dev/blog/mailgun-for-ai-agents/", "published_at": "2026-07-04 00:00:00+00:00", "updated_at": "2026-07-04 00:59:15.356722+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["MailKite", "Mailgun", "StackBlitz", "Node", "Python", "Ruby", "Go", "PHP"], "alternates": {"html": "https://wpnews.pro/news/the-mailgun-alternative-for-ai-agents", "markdown": "https://wpnews.pro/news/the-mailgun-alternative-for-ai-agents.md", "text": "https://wpnews.pro/news/the-mailgun-alternative-for-ai-agents.txt", "jsonld": "https://wpnews.pro/news/the-mailgun-alternative-for-ai-agents.jsonld"}}