# The Brevo alternative for AI agents

> Source: <https://mailkite.dev/blog/brevo-for-ai-agents/>
> Published: 2026-07-04 00:00:00+00:00

# 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.

Here’s the whole MailKite side: 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. 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. The same handler shape exists for Python, Ruby, Go, PHP, and Java; see the [receiving docs](/docs/receiving) and [sending docs](/docs/sending). 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](https://developers.brevo.com/docs/inbound-parse-webhooks) 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:

```
// 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. 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.

## 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=…" }
  ]
}
```

`From:`

is plain text and trivially forged, so an email body is untrusted input, a [prompt-injection vector](/blog/agent-inbox-security-by-design/), 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. Bring your own: 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: 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](/docs/sending) on `:587`

/`:465`

. The companion repo [ demo-brevo-ai-agent](https://github.com/mailkite/demo-brevo-ai-agent) runs both loops end to end;

[open it in StackBlitz](https://stackblitz.com/github/mailkite/demo-brevo-ai-agent?file=server.mjs)and fire a sample event in your browser.

## 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](https://github.com/mailkite/demo-brevo-ai-agent) (or [run it in your browser](https://stackblitz.com/github/mailkite/demo-brevo-ai-agent?file=server.mjs)), then [point a domain at MailKite](/docs/quickstart) 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.*
