The SparkPost alternative for AI agents MailKite, a new email service, positions itself as an alternative to SparkPost for AI agents that need to read and reply to emails, offering a simpler inbound event format with pre-parsed authentication verdicts. SparkPost's Relay Webhooks require developers to unpack raw RFC822 email, while MailKite provides a single parsed event and a 20-line agent loop for autonomous email handling. The SparkPost alternative for AI agents SparkPost now Bird Email is an enterprise sender whose inbound is Relay Webhooks: it POSTs a batched JSON array of relay messages carrying the raw RFC822 email — base64 in some fields — and no normalized auth verdict. MailKite which we build gives an agent a real inbox as one parsed email.received event with an auth block and a receive→reply loop. For developers wiring an agent to email. For an agent that has to read mail, the shape of what arrives is the whole job. Here is the same inbound email on both sides: what SparkPost POSTs to your endpoint, and what MailKite hands your agent. The rest of the post is the honest version of this picture — where SparkPost genuinely wins, the inbound unpack it asks of you shown in SparkPost’s own idiom , and the ~20-line agent loop that is the entire MailKite side. Here’s the bring-your-own-agent loop, whole. 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 : 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; // Body is untrusted INPUT, never instructions. auth is a verdict, not raw headers. 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, it thinks, it answers. One email.received event, one message, and event.auth is already a verdict. The full runnable version lives in the demo repo https://github.com/mailkite/demo-sparkpost-ai-agent — open it in StackBlitz https://stackblitz.com/github/mailkite/demo-sparkpost-ai-agent?file=server.mjs real Node in a browser tab , or point a domain at MailKite and fire a real one. The identical handler exists for Python, Ruby, Go, PHP, and Java; see the receiving docs /docs/receiving and sending docs /docs/sending . Where SparkPost wins for agents, honestly SparkPost is a serious sending platform, and this post isn’t “SparkPost is bad.” If your agent’s real job is to send a lot of mail with tight deliverability, SparkPost has years of ISP relationships, mature analytics opens, clicks, bounces, engagement cohorts , suppression management, and the kind of throughput and IP-warming story that enterprise senders need. Under Bird it sits inside a broader omnichannel platform email, SMS, WhatsApp, voice , so if you’re already routing other channels through Bird, keeping email there is a reasonable call. And it does receive inbound — the Relay Webhooks feature is real and documented. The question for an agent isn’t “can it receive,” it’s “in what shape, and how much of the unpack is mine.” What SparkPost asks of an agent builder SparkPost’s inbound feature is Relay Webhooks . You create an inbound domain and point its MX at SparkPost’s relay hosts rx1 , rx2 , rx3.sparkpostmail.com , priority 10 , then register a relay webhook that POSTs to your endpoint. Setup is two API calls: 1. register the inbound domain curl -sX POST https://api.sparkpost.com/api/v1/inbound-domains \ -H "Authorization: $SPARKPOST KEY" -H "Content-Type: application/json" \ -d '{ "domain": "agent.example.com" }' 2. attach a relay webhook that POSTs parsed mail to you ports 80/443 only curl -sX POST https://api.sparkpost.com/api/v1/relay-webhooks \ -H "Authorization: $SPARKPOST KEY" -H "Content-Type: application/json" \ -d '{ "name": "agent-inbound", "target": "https://myapp.ai/hooks/sparkpost", "match": { "domain": "agent.example.com" } }' What lands on that endpoint is not one message. SparkPost POSTs a batched JSON array of msys.relay message objects, and each one carries the raw RFC822 email that you unpack yourself. Here’s the honest handler, in SparkPost’s own idiom sparkpost-contrast/handler.mjs https://github.com/mailkite/demo-sparkpost-ai-agent/blob/main/sparkpost-contrast/handler.mjs in the demo repo : // SparkPost Relay Webhook: a BATCHED array → de-batch → conditional base64 → parse MIME → dig auth import { simpleParser } from "mailparser"; export default async function handler req, res { res.sendStatus 200 ; // ack the whole batch fast for const { msys } of req.body { // it's an array — loop it yourself const m = msys?.relay message; if m continue; // raw message is in content.email rfc822 — base64 ONLY when the flag says so const raw = m.content.email rfc822 is base64 ? Buffer.from m.content.email rfc822, "base64" : m.content.email rfc822; const mail = await simpleParser raw ; // headers, body, attachments: your job const from = m.friendly from; // envelope + composed-from live in separate fields const to = m.rcpt to; // there is NO normalized auth verdict. Parse it out of the raw headers yourself: const authResults = mail.headers.get "authentication-results" ?? ""; const spfPass = /spf=pass/i.test authResults ; const dkimPass = /dkim=pass/i.test authResults ; // …then decide whether to trust this before the agent acts on it } } None of this is exotic. But look at what stands between “a mail arrived” and “the agent can act on it”: you de-batch the array, branch on email rfc822 is base64 and decode, run a MIME parser, reassemble sender identity from friendly from / msg from / rcpt to , and — the part that matters most for an agent — derive SPF/DKIM/DMARC yourself by regex-ing Authentication-Results out of the raw headers , because the relay payload gives you no normalized verdict. For an inbox where an LLM will follow what the mail says, that trust signal is the whole ballgame, and SparkPost leaves you to compute it. The comparison, no adjective inflation | SparkPost Bird | MailKite | | |---|---|---| | Inbound shape | Batched JSON array of relay message | One email.received event per message | | Raw message | RFC822 in email rfc822 , base64 when flagged | Decoded text / html fields | | Auth to the agent | None normalized; parse Authentication-Results yourself | auth{spf,dkim,dmarc,spam} verdict | | Reply + threading | Build From / In-Reply-To yourself | from: event.to 0 , inReplyTo: event.id | | Agent runtime | None sending-first ESP | BYO loop, or route action: 'agent' on a queue | | Getting started | Enterprise, sales-led onboarding | DNS-verify, then send; 3,000 msgs/mo free | | Positioning | Enterprise omnichannel email/SMS/WhatsApp | Developer inbound → webhook | The through-line: SparkPost wins enterprise sending and omnichannel breadth. MailKite wins the inbound-for-an-agent path — a parsed message, a trust verdict, and a reply that threads itself, instead of an array you unpack and an auth result you regex out of headers. What actually hits your agent’s webhook Same inbound email, delivered parsed. No de-batching, no conditional base64, no MIME parser, and the auth block is a verdict your agent can branch on directly: { "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": "