cd /news/ai-agents/the-sparkpost-alternative-for-ai-age… · home topics ai-agents article
[ARTICLE · art-47520] src=mailkite.dev ↗ pub= topic=ai-agents verified=true sentiment=· neutral

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.

read9 min views1 publishedJul 4, 2026
The SparkPost alternative for AI agents
Image: Mailkite (auto-discovered)

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 repoopen 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:

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" }'

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

followwhat 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": "<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.

── more in #ai-agents 4 stories · sorted by recency
── more on @sparkpost 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/the-sparkpost-altern…] indexed:0 read:9min 2026-07-04 ·