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):
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 — open it in StackBlitz (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 and sending docs.
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 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.
auth block.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": "<p>Looks good — approved!</p>",
"threadId": "<a1b2c3@mail.example.com>",
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "spam": "ham" },
"attachments": [
{ "id": "msg_2Hk9…:0", "filename": "po.pdf", "contentType": "application/pdf",
"size": 18213, "url": "https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…" }
]
}
That auth block is load-bearing. Inbound email is a prompt-injection surface — From: is plain text, so a sender can forge who they are and then simply tell your agent what to do. Checking auth before 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 and the agent-security post linked at the end.
Or let MailKite run the agent
Everything 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 is agent carries a free-text agentPrompt, 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 tool that threads via inReplyTo. Same parsed inbound edge, same auth verdict; the difference is just where the agent’s turns execute. Details in the inbox-agents docs. To start either way, DNS-verify a domain (SPF + DKIM to send, MX to receive) — no sandbox, no approval wait — per the quickstart.
FAQ
Can SparkPost receive inbound email?
Yes. SparkPost (now Bird Email) supports inbound through Relay Webhooks: you register an inbound domain, point its MX at rx1/rx2/rx3.sparkpostmail.com, and SparkPost POSTs a batched JSON array of msys.relay_message objects to your endpoint. Each carries the raw RFC822 message in content.email_rfc822. It receives fine; the work is that you de-batch, decode, and parse it yourself. MailKite delivers one already-parsed email.received event instead.
Does SparkPost’s inbound webhook include SPF/DKIM/DMARC results?
Not 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, Received-SPF), 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 block with spf, dkim, dmarc, and a spam verdict.
Is SparkPost the same as Bird now?
Effectively 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, while the developer API reference still lives at developers.sparkpost.com and the inbound relay-webhook feature still uses the api.sparkpost.com surface. If you’re evaluating it, expect to cross both brands.
Is the raw inbound message always base64-encoded?
No — that’s a common trap. SparkPost base64-encodes content.email_rfc822 only when the message has content that isn’t safe to embed inline in JSON, and it flags that with email_rfc822_is_base64. In many payloads the flag is false and the raw message is inline. Your consumer has to branch on the boolean rather than assume base64, or you’ll mangle plain messages.
Does SparkPost have an agent inbox or agent runtime?
No. 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' route.
If 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 (or run it in your browser), then point a domain at MailKite and your agent’s next inbound email arrives as parsed JSON with an auth verdict.
Related: the pillar on giving your AI agent its own inbox, and agent inbox security by design.