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 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 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):
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 and sending docs.
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.
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, 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.
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 (or run it in your browser), then point a domain at MailKite and your agent reads and answers its own mail.
Related: the pillar on giving your agent an inbox and agent inbox security by design.