All posts
Gabe 18 min read

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.

MJ Mailjet vs MailKite
Mailjet vs MailKite — the same job (an inbox for your AI agent), two approaches.

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.

MJ Mailjet Parse API sender parse-in1 addrcreated via API your webhookMailjet field names map + verify DKIMyour job agent SpamAssassinScore is in the payload; a normalized SPF/DKIM/DMARC verdict is not. You read DKIM-Signature out of Headers and verify it yourself. MailKite sender MX edgeparse + auth JSON webhooksigned, with auth block agent the agent gets fields plus an auth verdict; the loop below is the whole integration
Getting one inbound email to an agent: Mailjet's Parse API vs MailKite. Same input, different amount left for you to assemble.

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.

  • EU data residency, by default. Mailjet was founded in Paris and stores customer data in the EU (Google Cloud in Frankfurt and Saint-Ghislain, Belgium), holds ISO 27001, and was an early GDPR-certified ESP. If your agent processes EU personal data and you want the mail infra inside the EU, that's a real, concrete answer rather than a checkbox.
  • A free tier that survived. After SendGrid dropped its free plan, Mailjet still offers 6,000 emails/month (capped at 200/day) with full API and SMTP access. For a low-volume agent that's room to build against.
  • Marketing plus transactional in one account. Sinch behind it, template editor, contact management, statistics. If the agent lives next to a marketing send program, one vendor covers both.
  • Inbound genuinely works. The Parse API decodes headers, text, HTML, and attachments for you and POSTs JSON. This is not "Mailjet can't receive." It can. The gap is what it hands the agent, and how much you assemble around it.

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.

MJ POST /parserouteprovision inbound via API parse-in1 addressan email arrives Mailjet POSTs JSONText-part, Html-part, From… map to your shaperename fields, parse addresses verify DKIM/SPFfrom raw Headers, yourself your agent logicfinally, decide and reply Every box above the blue one is yours to build and operate. On MailKite the edge does the parse and the auth, so the blue box is the only box.
The Mailjet Parse API path to an agent, stage by stage. MailKite folds the provision, map, and verify stages into the edge and ships an auth verdict with the JSON.

The comparison, agent-first

Mailjet Parse APIMailKite
Give an agent an inboxCreate a parseroute via API, get a parse-in1 addressPoint a domain, pick an address, set a webhook
Inbound deliveryJSON with Mailjet’s field namesParsed JSON, stable email.received shape
Auth verdict in payloadSpamAssassin score only; no SPF/DKIM/DMARC verdictauth: { spf, dkim, dmarc, spam }
Trust decisionVerify DKIM/SPF yourself from raw HeadersRead the auth block
Reply threadingYou build In-Reply-To and reply-from logicinReplyTo + reply from event.to[0]
Agent runtimeNone; host the loop yourselfBring your own, or a route with action: 'agent'
Data residencyEU by default (Frankfurt, Belgium)Aligned SPF/DKIM/DMARC on send
Free tier6,000/mo, 200/day cap3,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.

A spam score is not a trust verdictMailjet's payload gives you SpamAssassinScore, which is about unsolicited-mail likelihood, not sender authenticity. An agent that acts on instructions needs to know if the sender is who they claim, which is SPF, DKIM, and DMARC. On Mailjet that's your verification code; on MailKite it's the 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.

Discuss this post: Hacker News Share on X

Related posts