# The Mailjet alternative for AI agents

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

# The Mailjet alternative for AI agents

Mailjet (a Sinch company) can receive inbound mail through its Parse API, but you create the parseroute by hand, map its own field names, and there's no single trust verdict for the body. MailKite (which we build) gives an agent a real inbox: parsed JSON with an spf/dkim/dmarc block and a receive→reply loop. For developers wiring an autonomous email agent.

Start from what an autonomous agent actually needs: its own scoped address, mail arriving as data it can read, and a signal telling it whether that mail can be trusted before it acts. Mailjet was built to *send* well, and its inbound path, the Parse API, is a capable bolt-on rather than an agent surface. This post is the honest version of that gap: where Mailjet wins, the exact inbound shape it hands you, and the roughly 20 lines that are the entire MailKite side.

Everything below is a repo you can run while you read. Clone [ demo-mailjet-ai-agent](https://github.com/mailkite/demo-mailjet-ai-agent) and

`npm start`

, or [open it in StackBlitz](https://stackblitz.com/github/mailkite/demo-mailjet-ai-agent?file=server.mjs)(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 Mailjet path next to it in `mailjet-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 loop: 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`

):

``` 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) => {
  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. Trust comes from auth, not From.
  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, thinks, and answers, with the trust decision made from the `auth`

block instead of the forgeable `From`

. `npm start`

boots this 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 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](/docs/receiving) and [sending docs](/docs/sending).

## Where Mailjet wins for agents, honestly

Mailjet is a real ESP with a real inbound story, and a few things genuinely count in its favor.

## What Mailjet asks of an agent builder

Inbound on Mailjet is the Parse API, and the setup is deliberate: you create a *parseroute* with an API call. One `POST /v3/REST/parseroute`

with a webhook `Url`

returns an address on `@parse-in1.mailjet.com`

(or you can bind your own domain by pointing an MX record at `parse.mailjet.com`

). Every inbound email to that address is then POSTed to your `Url`

as `application/json`

.

The payload is Mailjet’s own field vocabulary, not a normalized one. You get `Sender`

, `Recipient`

, `From`

, `To`

, `Cc`

, `Subject`

, `Date`

, a `Headers`

object, a `Parts`

array, `Text-part`

and `Html-part`

for the body, `SpamAssassinScore`

, and attachments as base64 in `AttachmentN`

fields. What you don’t get is a single trust verdict. There’s no `spf`

, `dkim`

, or `dmarc`

field. The `DKIM-Signature`

sits raw inside `Headers`

, and if the agent’s decision to act depends on whether the sender is really the sender, that verification is your code. Here’s the honest inbound handler — it’s `mailjet-contrast/handler.mjs`

in the repo, sitting right next to the MailKite `server.mjs`

above so you can put them side by side:

```
// Mailjet Parse API inbound: you create the parseroute, then map + verify yourself.
import express from "express";
const app = express();

app.use("/mailjet/parse", express.json({ limit: "30mb" }));

app.post("/mailjet/parse", (req, res) => {
  const m = req.body; // Mailjet's field names, not yours

  const email = {
    from: m.From,                 // "Ada <ada@example.com>" — parse the address out
    to: m.Recipient,
    subject: m.Subject,
    text: m["Text-part"],         // hyphenated keys
    html: m["Html-part"],
    spamScore: m.SpamAssassinScore,
  };

  // No spf/dkim/dmarc verdict in the payload. If trust matters, do it yourself:
  const dkimHeader = m.Headers?.["DKIM-Signature"]; // raw signature string
  const trusted = verifyDkim(dkimHeader, m.Headers); // you write/import this

  // Attachments arrive base64 under Attachment1, Attachment2, … in Parts.
  // Decode and map before your agent ever sees a file.

  res.sendStatus(200);
  runAgent({ task: email.text, from: email.from, trusted });
});

app.listen(3000);
```

None of this is exotic. But between “an email arrived” and “the agent can safely decide what to do about it” sits a parseroute you provision by API, a field-name mapping layer, and a DKIM/SPF verification step the payload doesn’t do for you. Mailjet also has no built-in agent runtime: it’s a send-first ESP, so the model loop, the reply threading, and the guardrails are all yours to host.

The repo makes that fragility concrete. `npm test`

feeds the header-grep Outlook’s `Received-SPF: Pass (…)`

— a genuine SPF pass with no `spf=pass`

token in `Authentication-Results`

— and watches it return `false`

, scoring a legitimately authenticated sender as untrusted. A second test makes the other half explicit: the one number Mailjet *does* hand you, `SpamAssassinScore`

, is spam likelihood, not an authentication pass. Both are shaky ground for a *trust* decision, which is the check that matters most for an autonomous agent.

## The comparison, agent-first

| Mailjet Parse API | MailKite | |
|---|---|---|
| Give an agent an inbox | Create a parseroute via API, get a parse-in1 address | Point a domain, pick an address, set a webhook |
| Inbound delivery | JSON with Mailjet’s field names | Parsed JSON, stable `email.received` shape |
| Auth verdict in payload | SpamAssassin score only; no SPF/DKIM/DMARC verdict | `auth: { spf, dkim, dmarc, spam }` |
| Trust decision | Verify DKIM/SPF yourself from raw Headers | Read the `auth` block |
| Reply threading | You build `In-Reply-To` and reply-from logic | `inReplyTo` + reply from `event.to[0]` |
| Agent runtime | None; host the loop yourself | Bring your own, or a route with `action: 'agent'` |
| Data residency | EU by default (Frankfurt, Belgium) | Aligned SPF/DKIM/DMARC on send |
| Free tier | 6,000/mo, 200/day cap | 3,000 messages/mo, in + out |

The through-line: Mailjet can receive, and its EU posture and free tier are real advantages. MailKite’s edge is the shape it hands an agent, a stable parsed event with a trust verdict already computed, and a place to run the loop.

## What actually hits your agent’s webhook

MailKite decodes the message at the edge, so your handler gets fields and a verdict, not MIME and raw headers:

```
{
  "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 Mailjet Parse path makes you reconstruct from raw headers:

The `auth`

block is the load-bearing difference for an agent. `From:`

is plain text and trivially forged, so a naive loop that reads the body as instructions is a prompt-injection target. The verdict lets you gate on whether SPF, DKIM, and DMARC passed before you weight a sender at all. Checking it is necessary, not sufficient; the real defense is architectural, and it’s the whole subject of [the agent-security post](/blog/agent-inbox-security-by-design/).

`auth`

block.MailKite, which we build, gives an agent a real inbox as its native shape: a scoped address on a domain you control, inbound decoded to the JSON above, and two ways to run the loop — both in the repo. Bring your own (`server.mjs`

, the handler up top) when the brain is your code, or let MailKite run it (`managed-route.mjs`

): set a route whose `action`

is `agent`

with an `agentPrompt`

and the model loop runs on a durable queue, capped, reaped on timeout, and recorded as a transcript you can drill into. Same parsed edge, same trust verdict, either way. If you cloned [ demo-mailjet-ai-agent](https://github.com/mailkite/demo-mailjet-ai-agent),

`managed-route.mjs`

dry-runs the route creation and `mailjet-contrast/handler.mjs`

is the same job the Mailjet 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 Mailjet receive inbound email for an agent?**
Yes. Mailjet’s Parse API receives inbound: you create a parseroute (`POST /v3/REST/parseroute`

) pointing an address to your webhook, and Mailjet POSTs each inbound email as JSON with fields like `From`

, `Subject`

, `Text-part`

, `Html-part`

, and `SpamAssassinScore`

. The agent gap is that it uses Mailjet’s field naming and doesn’t include a normalized SPF/DKIM/DMARC verdict, so trust decisions are your code.

**Does the Mailjet Parse API include SPF, DKIM, or DMARC results?**
Not as a normalized verdict. The payload includes a SpamAssassin score, and the raw `DKIM-Signature`

lives inside the `Headers`

object, but there’s no `spf`

/`dkim`

/`dmarc`

field. If your agent’s action depends on sender authenticity, you verify it yourself. MailKite ships that verdict in an `auth`

block on every inbound event.

**Is Mailjet a good fit if I need EU data residency?**
It’s one of its strengths. Mailjet was founded in Paris, stores data in the EU (Frankfurt and Belgium), holds ISO 27001, and was an early GDPR-certified ESP, now under Sinch. If EU residency is a hard requirement and you’re comfortable assembling the agent inbound layer, that’s a genuine reason to pick it.

**Does Mailjet have a built-in agent or inbox-agent runtime?**
No. Mailjet is a send-first transactional and marketing ESP; there’s no built-in model loop for inbound. You host the receive→think→reply loop yourself. MailKite offers both a bring-your-own webhook loop and a managed `action: 'agent'`

route that runs the loop for you.

**How do I move an agent from Mailjet Parse to MailKite?**
Point a domain at MailKite (SPF + DKIM to send, MX to receive), pick an address like `agent@yourco.dev`

, and set the webhook to the loop at the top of this post. Inbound arrives as the `email.received`

JSON with an `auth`

block, and replies thread with `inReplyTo`

. No sandbox or approval wait; see the [quickstart](/docs/quickstart).

Mailjet can receive, and its EU footing is real. But an autonomous agent wants its inbound already parsed into a stable shape with a trust verdict attached, and a place to run the loop. Clone the [demo repo](https://github.com/mailkite/demo-mailjet-ai-agent) (or [run it in your browser](https://stackblitz.com/github/mailkite/demo-mailjet-ai-agent?file=server.mjs)), then [point a domain at MailKite](/docs/quickstart) and your agent reads and answers its own mail.

*Related: the pillar on giving your agent an inbox and agent inbox security by design.*
