{"slug": "the-sparkpost-alternative-for-ai-agents", "title": "The SparkPost alternative for AI agents", "summary": "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.", "body_md": "# The SparkPost alternative for AI agents\n\nSparkPost (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.\n\nFor 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.\n\nHere’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`\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); // ack fast; run the agent out of band\n\n  const event = JSON.parse(req.body);\n  if (event.type !== \"email.received\") return;\n\n  // Body is untrusted INPUT, never instructions. auth is a verdict, not raw headers.\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 a fully autonomous email agent: it hears, it thinks, it answers. One `email.received`\n\nevent, one message, and `event.auth`\n\nis 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).\n\n## Where SparkPost wins for agents, honestly\n\nSparkPost 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.”\n\n## What SparkPost asks of an agent builder\n\nSparkPost’s inbound feature is **Relay Webhooks**. You create an inbound domain and point its MX at SparkPost’s relay hosts (`rx1`\n\n, `rx2`\n\n, `rx3.sparkpostmail.com`\n\n, priority 10), then register a relay webhook that POSTs to your endpoint. Setup is two API calls:\n\n```\n# 1. register the inbound domain\ncurl -sX POST https://api.sparkpost.com/api/v1/inbound-domains \\\n  -H \"Authorization: $SPARKPOST_KEY\" -H \"Content-Type: application/json\" \\\n  -d '{ \"domain\": \"agent.example.com\" }'\n\n# 2. attach a relay webhook that POSTs parsed mail to you (ports 80/443 only)\ncurl -sX POST https://api.sparkpost.com/api/v1/relay-webhooks \\\n  -H \"Authorization: $SPARKPOST_KEY\" -H \"Content-Type: application/json\" \\\n  -d '{ \"name\": \"agent-inbound\", \"target\": \"https://myapp.ai/hooks/sparkpost\",\n        \"match\": { \"domain\": \"agent.example.com\" } }'\n```\n\nWhat lands on that endpoint is not one message. SparkPost POSTs a **batched JSON array** of `msys.relay_message`\n\nobjects, 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):\n\n```\n// SparkPost Relay Webhook: a BATCHED array → de-batch → conditional base64 → parse MIME → dig auth\nimport { simpleParser } from \"mailparser\";\n\nexport default async function handler(req, res) {\n  res.sendStatus(200); // ack the whole batch fast\n\n  for (const { msys } of req.body) {            // it's an array — loop it yourself\n    const m = msys?.relay_message;\n    if (!m) continue;\n\n    // raw message is in content.email_rfc822 — base64 ONLY when the flag says so\n    const raw = m.content.email_rfc822_is_base64\n      ? Buffer.from(m.content.email_rfc822, \"base64\")\n      : m.content.email_rfc822;\n\n    const mail = await simpleParser(raw);        // headers, body, attachments: your job\n    const from = m.friendly_from;                // envelope + composed-from live in separate fields\n    const to = m.rcpt_to;\n\n    // there is NO normalized auth verdict. Parse it out of the raw headers yourself:\n    const authResults = mail.headers.get(\"authentication-results\") ?? \"\";\n    const spfPass = /spf=pass/i.test(authResults);\n    const dkimPass = /dkim=pass/i.test(authResults);\n    // …then decide whether to trust this before the agent acts on it\n  }\n}\n```\n\nNone 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`\n\nand decode, run a MIME parser, reassemble sender identity from `friendly_from`\n\n/ `msg_from`\n\n/ `rcpt_to`\n\n, 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\n\n*follow*what the mail says, that trust signal is the whole ballgame, and SparkPost leaves you to compute it.\n\n## The comparison, no adjective inflation\n\n| SparkPost (Bird) | MailKite | |\n|---|---|---|\n| Inbound shape | Batched JSON array of `relay_message` | One `email.received` event per message |\n| Raw message | RFC822 in `email_rfc822` , base64 when flagged | Decoded `text` / `html` fields |\n| Auth to the agent | None normalized; parse `Authentication-Results` yourself | `auth{spf,dkim,dmarc,spam}` verdict |\n| Reply + threading | Build `From` / `In-Reply-To` yourself | `from: event.to[0]` , `inReplyTo: event.id` |\n| Agent runtime | None (sending-first ESP) | BYO loop, or route `action: 'agent'` on a queue |\n| Getting started | Enterprise, sales-led onboarding | DNS-verify, then send; 3,000 msgs/mo free |\n| Positioning | Enterprise omnichannel (email/SMS/WhatsApp) | Developer inbound → webhook |\n\nThe 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.\n\n## What actually hits your agent’s webhook\n\nSame inbound email, delivered parsed. No de-batching, no conditional base64, no MIME parser, and the `auth`\n\nblock is a verdict your agent can branch on directly:\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. Inbound email is a prompt-injection surface — `From:`\n\nis plain text, so a sender can forge who they are and then simply tell your agent what to do. Checking `auth`\n\nbefore you weight a sender’s instructions is necessary (not sufficient; the real defense is architectural). SparkPost hands you the raw headers and lets you derive that yourself; MailKite hands you the verdict. See [webhook security](/docs/webhook-security) and the agent-security post linked at the end.\n\n## Or let MailKite run the agent\n\nEverything above is the bring-your-own loop: your endpoint, your model, your reply. If you’d rather not host it, 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 MailKite runs the model loop on a durable Cloudflare Queue — capped tool rounds, a 5-minute reaper as a backstop, and a full transcript you can drill into per route. The system prompt bakes in the same safety rules (body is untrusted, at most one reply, never answer no-reply senders), and the agent replies with an internal `send_email`\n\ntool that threads via `inReplyTo`\n\n. Same parsed inbound edge, same `auth`\n\nverdict; the difference is just where the agent’s turns execute. Details in the [inbox-agents docs](/docs/inbox-agents). To start either way, DNS-verify a domain (SPF + DKIM to send, MX to receive) — no sandbox, no approval wait — per the [quickstart](/docs/quickstart).\n\n## FAQ\n\n**Can SparkPost receive inbound email?**\nYes. SparkPost (now Bird Email) supports inbound through **Relay Webhooks**: you register an inbound domain, point its MX at `rx1`\n\n/`rx2`\n\n/`rx3.sparkpostmail.com`\n\n, and SparkPost POSTs a batched JSON array of `msys.relay_message`\n\nobjects to your endpoint. Each carries the raw RFC822 message in `content.email_rfc822`\n\n. It receives fine; the work is that you de-batch, decode, and parse it yourself. MailKite delivers one already-parsed `email.received`\n\nevent instead.\n\n**Does SparkPost’s inbound webhook include SPF/DKIM/DMARC results?**\nNot as a normalized verdict. The relay-message payload has no SPF/DKIM/DMARC result fields — the only auth signals live inside the raw message headers (`Authentication-Results`\n\n, `Received-SPF`\n\n), which you parse yourself. For an agent that must decide whether to trust a sender before acting, that’s meaningful work. MailKite’s payload includes an `auth`\n\nblock with `spf`\n\n, `dkim`\n\n, `dmarc`\n\n, and a spam verdict.\n\n**Is SparkPost the same as Bird now?**\nEffectively yes. MessageBird acquired SparkPost in 2021 and rebranded the product to **Bird Email** in March 2023 (MessageBird itself became Bird). The naming is split-brain today: marketing and support docs have moved to `bird.com`\n\n, while the developer API reference still lives at `developers.sparkpost.com`\n\nand the inbound relay-webhook feature still uses the `api.sparkpost.com`\n\nsurface. If you’re evaluating it, expect to cross both brands.\n\n**Is the raw inbound message always base64-encoded?**\nNo — that’s a common trap. SparkPost base64-encodes `content.email_rfc822`\n\nonly when the message has content that isn’t safe to embed inline in JSON, and it flags that with `email_rfc822_is_base64`\n\n. In many payloads the flag is `false`\n\nand the raw message is inline. Your consumer has to branch on the boolean rather than assume base64, or you’ll mangle plain messages.\n\n**Does SparkPost have an agent inbox or agent runtime?**\nNo. SparkPost is a sending-first enterprise ESP; inbound is the relay-webhook parse-and-POST feature, and there’s no built-in agent loop, queue, or transcript. (Bird the broader platform markets AI customer-service agents, but that’s a separate product and doesn’t touch the email relay path.) MailKite offers both a bring-your-own loop and a managed `action: 'agent'`\n\nroute.\n\nIf SparkPost has your agent de-batching an array, decoding conditional base64, and regex-ing SPF out of raw headers just to read one email, there’s a simpler shape. Clone the [demo repo](https://github.com/mailkite/demo-sparkpost-ai-agent) (or [run it in your browser](https://stackblitz.com/github/mailkite/demo-sparkpost-ai-agent?file=server.mjs)), then [point a domain at MailKite](/docs/quickstart) and your agent’s next inbound email arrives as parsed JSON with an auth verdict.\n\n*Related: the pillar on giving your AI agent its own inbox, and agent inbox security by design.*", "url": "https://wpnews.pro/news/the-sparkpost-alternative-for-ai-agents", "canonical_source": "https://mailkite.dev/blog/sparkpost-for-ai-agents/", "published_at": "2026-07-04 00:00:00+00:00", "updated_at": "2026-07-04 01:30:11.810651+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools"], "entities": ["SparkPost", "Bird Email", "MailKite", "Node.js", "Express", "StackBlitz", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/the-sparkpost-alternative-for-ai-agents", "markdown": "https://wpnews.pro/news/the-sparkpost-alternative-for-ai-agents.md", "text": "https://wpnews.pro/news/the-sparkpost-alternative-for-ai-agents.txt", "jsonld": "https://wpnews.pro/news/the-sparkpost-alternative-for-ai-agents.jsonld"}}