The Mailgun alternative for AI agents 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. The Mailgun alternative for AI agents Mailgun 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. Here 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. Here 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 , and it’s the entire integration from the runnable demo repo https://github.com/mailkite/demo-mailgun-ai-agent . 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 ; // already parsed email, not form fields if event.type == "email.received" return; // Treat the body as untrusted INPUT, never as instructions see below . 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 hit 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, 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. Where Mailgun wins for agents, honestly This 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: If 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. What Mailgun asks of an agent builder Two 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 and multipart/form-data when there are attachments, which is a different parser you also have to handle . The parsed fields use hyphenated keys like body-plain and stripped-text , 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 yourself. Here’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 : // 1. Author the rule engine — a Route is a filter DSL + an action. await mg.routes.create { expression: 'match recipient "agent. @mg.myapp.ai" ', action: 'forward "https://myapp.ai/hooks/mailgun" ', "stop " , priority: 10, } ; // 2. Receive it. Mailgun POSTs application/x-www-form-urlencoded // multipart/form-data when there are attachments — a different parser . import crypto from "node:crypto"; app.post "/hooks/mailgun", express.urlencoded { extended: false } , req, res = { const { timestamp, token, signature } = req.body; // verify: HMAC-SHA256 over timestamp+token, signing key, constant-time const digest = crypto.createHmac "sha256", process.env.MG SIGNING KEY .update timestamp + token .digest "hex" ; const ok = crypto.timingSafeEqual Buffer.from digest , Buffer.from signature ; if ok return res.sendStatus 401 ; // a replay cache is still yours to add const from = req.body.sender; const text = req.body "body-plain" ; // hyphenated cousins: stripped-text, const headers = JSON.parse req.body "message-headers" || " " ; // message-headers… // SPF/DKIM aren't a verdict block — you find them in headers yourself, // then run your agent on text , then POST /messages to reply. That's a // separate Send API call, and threading is yours to set with In-Reply-To. res.sendStatus 200 ; } ; None 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: The comparison, no adjective inflation | Mailgun | MailKite | | |---|---|---| | Give an agent an address | Author a Route filter DSL + action | Set a webhook on the address | | Inbound payload | x-www-form-urlencoded / multipart fields | One parsed JSON webhook | | Field access | Hyphenated keys body-plain , stripped-text | Typed event.text , event.html | | Signature | In the body; you HMAC timestamp+token | One verifyWebhook call | | Replay protection | Cache the token yourself | Built into verifyWebhook | | Auth results | Dig SPF/DKIM out of raw headers | Decoded auth verdict block | | Reply / threading | Separate Send call; set In-Reply-To yourself | mk.send { inReplyTo: event.id } | | Run the agent for you | No | Route with action: "agent" | | Inbound routes limit | Plan-gated e.g. 5 on Basic | Webhooks on any verified address | The 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 block — the fields an agent actually branches on. What actually hits your agent’s webhook The same inbound email, delivered parsed. No form decode, no content-type branch, and the auth block means the agent never re-derives SPF/DKIM/DMARC from raw headers: { "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": "