The Brevo alternative for AI agents
Brevo (formerly Sendinblue) is a marketing platform with a transactional API, and it can receive: Inbound Parsing POSTs email as an items[] array. But there's no normalized SPF/DKIM/DMARC verdict and no agent loop, so you rebuild both. MailKite (which we build) hands an autonomous agent a real inbox as one parsed email.received event with an auth block and a receive→reply loop.
An autonomous agent doesn’t need a campaign designer or a CRM. It needs one thing most send-first platforms never shipped: its own real address that it can read from, decide on, and reply to with no human in the loop. Brevo can receive mail, so this isn’t a “can’t.” It’s a question of shape, and of what you rebuild after the JSON lands. Here’s the contrast in one picture.
Everything below is a repo you can run while you read. Clone demo-brevo-ai-agent and npm start, or open it in StackBlitz (real Node in a browser tab, no account) where it runs on load. You don’t need a domain to see the loop run: a webhook normally needs a public URL, so npm start boots the server and self-fires a correctly signed email.received event at its own localhost — the whole receive→verify→think→reply loop runs in one command (the reply is a dry-run, and managed-route.mjs dry-runs the hosted path too). It also ships the Brevo path next to it in brevo-contrast/, so every contrast here is something you can diff and test, not take on faith. A domain comes in only when you want real inbound email to arrive.
Here’s the whole MailKite side: email in, verify the signature, hand the body to your model, reply through the same client. This is the heart of server.mjs in that repo — the repo wraps it in a dry-run harness and a stub agent so it runs with no key and no LLM, but the loop is exactly this, and 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. Weight it by the auth verdict.
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 the receive→think→reply loop, whole. npm start boots the server and self-fires the exact payload MailKite’s delivery worker sends, so in one command you watch the agent read the task, take its trust verdict straight off event.auth, and dry-run the reply — no account or LLM required. The same handler shape exists for Python, Ruby, Go, PHP, and Java; see the receiving docs and sending docs. The rest of this post is the honest version of the diagram: where Brevo genuinely fits an agent, what its inbound path asks you to build, and what the parsed payload actually contains.
Where Brevo wins for agents, honestly
Brevo is a marketing platform first: email campaigns, a built-in CRM, marketing automation, SMS, and WhatsApp, with a transactional email API bolted alongside. That breadth is the point of the pitch, and for one kind of agent it’s the right tool.
If your agent’s job lives near marketing, having all of it behind one API key is real leverage. An agent that qualifies a lead, updates a contact attribute, and then triggers a lifecycle sequence can do all three against the same Brevo account it reads inbound replies from. You don’t wire a CRM and an ESP and an inbox parser together; they’re already one system.
Brevo’s inbound parse also does something genuinely useful for LLM input. Each parsed item carries an ExtractedMarkdownMessage field: the reply body cleaned up, with the quoted history and the signature split off into a separate ExtractedMarkdownSignature. If you’ve ever fed a raw email reply to a model and watched it dutifully summarize the person’s phone number and legal disclaimer, you know why that’s worth something. That extraction is nicer than what a lot of inbound APIs hand back.
So this isn’t “Brevo can’t receive.” It can. The question is what the shape costs an agent builder.
What Brevo asks of an agent builder
Brevo’s Inbound Parsing works by DNS: you add MX records pointing a subdomain (their example is reply.yourdomain.com) at inbound1.sendinblue.com and inbound2.sendinblue.com, then register a webhook by POSTing to /v3/webhooks with type: "inbound" and the inboundEmailProcessed event. From then on Brevo POSTs an object with an items array; each item has From, To, Subject, RawTextBody, RawHtmlBody, ExtractedMarkdownMessage, Attachments, Headers, and a SpamScore. (There’s a polling path too, but mind the gotcha: GET /v3/inbound/events returns metadata and delivery logs, not the parsed body. Only the webhook POST carries RawTextBody and the markdown extraction, so an agent that wants the content has to receive the push.)
Two things follow from that shape once an agent is the consumer.
First, it’s a batch. You iterate items, not “handle this one email.” Fine, but your loop owns it.
Second, and this is the load-bearing one: there is no normalized authentication verdict. SpamScore is a float from rspamd, not a pass/fail on SPF, DKIM, or DMARC. The actual SPF and DKIM results exist only as raw header lines (Received-SPF, Authentication-Results, ARC-Seal) inside the Headers field. If your agent is going to act on an email, it has to reconstruct the trust verdict itself, by string-matching headers, before it decides how much weight to give a sender’s instructions. Here’s that reconstruction — it’s brevo-contrast/handler.mjs in the same repo, sitting right next to the MailKite server.mjs above so you can put them side by side:
// Brevo inbound: POST { items: [...] } — parsed, but the auth verdict isn't a field.
import express from "express";
const app = express();
app.use(express.json());
app.post("/hooks/brevo", async (req, res) => {
res.sendStatus(200); // guard this endpoint yourself: a secret path or IP allowlist
for (const item of req.body.items ?? []) {
const body = item.ExtractedMarkdownMessage ?? item.RawTextBody ?? "";
// SpamScore is a number, not a pass/fail. There is no spf/dkim/dmarc field.
// Reconstruct the verdict from the raw headers before trusting the sender:
const rawHeaders = JSON.stringify(item.Headers ?? "");
const spfPass = /spf=pass/i.test(rawHeaders);
const dmarcPass = /dmarc=pass/i.test(rawHeaders);
const answer = await runAgent({
task: body,
from: item.From?.Address,
trusted: spfPass && dmarcPass,
});
// Reply is a separate product surface: the transactional send API.
// Set In-Reply-To / References yourself if you want it to thread.
await brevoSendTransactional({ to: item.From?.Address, ...answer });
}
});
Grepping Authentication-Results for spf=pass is exactly the kind of thing that looks fine in a demo and quietly rots: the header format varies by upstream relay, dmarc= isn’t always present, and a regex over headers an attacker partly controls is a shaky foundation for a trust decision. The repo makes that failure concrete — npm test feeds this grep Outlook’s Received-SPF: Pass (…), a genuine SPF pass with no spf=pass token in it, and watches the function return false. A legitimately authenticated sender, scored untrusted, because the header format varied by relay. It’s doable. It’s just yours to get right, and it’s the check that matters most for an autonomous agent.
The reply is also a separate concern. Inbound Parsing gets mail in; sending the answer is the transactional email API, a different surface (a different product to activate, in fact: new accounts open a support ticket before transactional sending turns on), and threading (In-Reply-To, References) is on you to set. None of this is exotic. It’s a marketing suite that happens to expose an inbound parser, not a receive→reply loop for a bot.
event.auth at the edge.The comparison, agent-relevant rows only
| Brevo | MailKite | |
|---|---|---|
| Product center of gravity | Marketing suite (campaigns, CRM, automation) | Inbound email → webhook |
| Inbound shape | items[] array (batch) via webhook or poll | One email.received event |
| Auth verdict (SPF/DKIM/DMARC) | Not a field — grep raw Headers yourself | Normalized auth block |
| Spam signal | SpamScore float (rspamd) | spam: ham/spam plus auth |
| Body for the model | ExtractedMarkdownMessage (clean, nice) | Decoded text + html |
| Agent loop built in | None — wire model + transactional send | Optional route action: agent, or BYO |
| Reply + threading | Separate transactional API; set headers yourself | mk.send({ inReplyTo }) resolves it |
| Webhook auth | Guard the endpoint yourself | Signed; verifyWebhook() in one call |
| Start | Add MX + create webhook via API | DNS-verify, one webhook |
| Free tier | 300 emails/day | 3,000 messages/mo (in + out) |
The through-line: Brevo wins when the agent’s work is marketing-shaped and you want CRM, campaigns, and inbound under one roof. MailKite wins when the agent’s work is “own an address, read what arrives, decide, reply safely,” because the trust verdict and the reply path are already assembled.
What actually hits your agent’s webhook
Here’s the MailKite email.received event: decoded body, a resolved threadId, attachments as short-lived signed URLs, and the auth block that the Brevo path made you reconstruct.
{
"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=…" }
]
}
Don’t take the shape on faith — fire one and watch it come back, auth block and all. Spoof the SPF/DKIM to fail and see the verdict flip; that’s the field the Brevo path makes you reconstruct from raw headers:
From: is plain text and trivially forged, so an email body is untrusted input, a prompt-injection vector, not instructions. The auth block is how the agent decides how much to trust a sender before it acts, without string-parsing a header itself. It’s necessary, not sufficient; the real defense is bounding what a fooled agent can do, but starting from a normalized verdict beats starting from a regex.
Two ways to run it on MailKite
MailKite, which we build, gives an agent a scoped address on a domain you control (agent@yourco.dev), and runs the loop one of two ways — both in the repo. Bring your own (server.mjs): the inbound webhook hits your endpoint, your model runs, you reply with mk.send(), which is the code at the top. Or let MailKite run it (managed-route.mjs): create a route whose action is agent with an agentPrompt, and the model loop runs on a durable Cloudflare Queue with an agent_runs ledger and a per-route transcript you can drill into, capped and reaped so a slow model call can’t wedge ingest. Either way there’s no IMAP on a bot, no shared personal Gmail, no OAuth token-refresh churn, and no MIME parsing. To start, DNS-verify the domain (SPF + DKIM to send, MX to receive); there’s no sandbox or approval wait, the free tier is 3,000 messages a month across inbound and outbound, and SMTP-only apps can send through the submission edge on :587/:465. If you cloned demo-brevo-ai-agent at the top, managed-route.mjs dry-runs the route creation and brevo-contrast/handler.mjs is the same job the Brevo way — npm test runs both sides so you can watch the header-grep miss a real pass before you decide which shape you want.
FAQ
Can Brevo receive inbound email?
Yes. Brevo’s Inbound Parsing points a subdomain’s MX at inbound1/inbound2.sendinblue.com and POSTs parsed mail as an items[] array (or you poll the inbound events endpoint). Each item has the body, Headers, Attachments, and a SpamScore. MailKite delivers the same message as a single email.received event with a normalized auth block.
Does Brevo’s inbound payload include an SPF/DKIM/DMARC verdict?
Not as a field. SpamScore is a spam float, and the SPF/DKIM results live only as raw lines (Received-SPF, Authentication-Results) inside Headers, so you parse the verdict yourself. MailKite normalizes it into auth: { spf, dkim, dmarc, spam } at the edge.
Is Brevo good for AI agents?
For a marketing-shaped agent, yes: campaigns, CRM, automation, and transactional send behind one API key is real leverage, and ExtractedMarkdownMessage gives the model a clean body. For an agent whose core job is receive→decide→reply safely, you’ll rebuild the trust verdict, the reply threading, and the loop. MailKite ships those.
What does Brevo cost versus MailKite? Brevo’s free plan is 300 emails/day with paid transactional plans from around $9/mo; marketing automation caps contacts on lower tiers. MailKite’s free tier is 3,000 messages/month across inbound and outbound with no per-domain fee. Confirm current numbers on each pricing page before you commit.
Can I keep sending marketing email on Brevo and run the agent on MailKite?
Yes. They’re not exclusive. Run campaigns and lifecycle automation on Brevo, and point the agent’s address (a subdomain or a separate domain) at MailKite so it receives parsed JSON with an auth block and replies over the Send API.
If your agent is fishing an SPF result out of raw headers before it dares trust an email, that’s the seam. Clone the demo repo (or run it in your browser), then point a domain at MailKite and your agent’s next inbound email arrives parsed, authenticated, and ready to answer.
Related: the pillar on giving your agent its own inbox, and agent inbox security by design.