Mailjet Parse API
Inbound parsing turns the email your users send into structured data your app can act on: a reply becomes a support ticket, a forwarded PDF becomes a row in your database, an incoming message becomes a task for an agent. This guide walks through Mailjet's Parse API end to end, then shows the same flow on MailKite.
What you'll need
- A domain you control, with access to its DNS records.
- A public HTTPS endpoint to receive the webhook (a tunnel like ngrok works while testing).
- A Mailjet account with an API key and secret key for the first half.
Part 1 — Receive and parse email with Mailjet
1. Point a subdomain at Mailjet
Inbound parsing works on a dedicated receiving subdomain — commonly
parse.yourdomain.com. Add an MX record so mail sent
there is delivered to Mailjet's parse servers instead of your own:
Host Type Priority Value
parse.yourdomain.com MX 10 parse.mailjet.com.
DNS can take a few hours to propagate, so add this early. If you just want to
try things out, Mailjet also hands you a ready-made address on
parse-in1.mailjet.com — no DNS required. Any address on your
receiving subdomain will flow into Mailjet once the record resolves.
2. Create a parse route
A parseroute maps an inbound address to your webhook. Create one
from the Mailjet dashboard, or POST to the REST endpoint with your
key and secret as HTTP basic auth:
curl -X POST https://api.mailjet.com/v3/REST/parseroute \
-u "$MJ_APIKEY_PUBLIC:$MJ_APIKEY_PRIVATE" \
-H "Content-Type: application/json" \
-d '{
"Url": "https://yourapp.com/hooks/mailjet",
"Email": "catchall@parse.yourdomain.com"
}'
Set Url to your endpoint and Email to the address you
want parsed. Leave Email off and Mailjet returns an auto-generated
address on parse-in1.mailjet.com you can use right away. Every
message to that address is now POSTed to your Url.
3. Parse the payload
Mailjet POSTs a JSON object — one per message. It carries
Sender and Recipient, the raw From,
To, Cc and Subject lines, both
Text-part and Html-part bodies, the full
Headers, a SpamAssassinScore, and a
Parts array whose ContentRef entries point at each
body and attachment:
{
"Sender": "ada@example.com",
"Recipient": "catchall@parse.yourdomain.com",
"Date": "20260722T101500",
"From": "Ada Lovelace <ada@example.com>",
"To": "catchall@parse.yourdomain.com",
"Cc": "",
"Subject": "Can't update my card",
"Text-part": "Hi — my payment keeps failing…",
"Html-part": "<p>Hi — my payment keeps failing…</p>",
"Headers": {
"From": ["Ada Lovelace <ada@example.com>"],
"Subject": ["Can't update my card"],
"Content-Type": ["multipart/mixed; boundary=\"===part===\""]
},
"Parts": [
{
"ContentRef": "Text-part",
"Headers": { "Content-Type": ["text/plain; charset=UTF-8"] }
},
{
"ContentRef": "Attachment1",
"Headers": {
"Content-Type": ["image/png; name=\"screenshot.png\""],
"Content-Disposition": ["attachment; filename=\"screenshot.png\""]
}
}
],
"SpamAssassinScore": "0.7",
"Attachment1": "iVBORw0KGgoAAAANSUhEUgAA…"
}
Attachments aren't links — their content is Base64-encoded inline on
Attachment1, Attachment2, and so on. Read the fields
you care about, then walk Parts to decode each one:
import express from "express";
const app = express();
// Mailjet POSTs one message as JSON. Attachments arrive Base64-inlined,
// so give the parser room.
app.use(express.json({ limit: "50mb" }));
app.post("/hooks/mailjet", (req, res) => {
const msg = req.body;
const body = msg["Text-part"] ?? msg["Html-part"];
console.log("from", msg.Sender, "·", msg.Subject);
// ...open a ticket, reply, hand to an agent.
// Attachments are Base64 strings on AttachmentN keys; Parts maps each
// ContentRef to its filename and content type.
for (const part of msg.Parts ?? []) {
const ref = part.ContentRef; // e.g. "Attachment1"
if (!ref?.startsWith("Attachment")) continue;
const bytes = Buffer.from(msg[ref], "base64");
// ...store bytes; filename lives in part.Headers["Content-Disposition"].
}
res.sendStatus(200);
});
That's inbound parsing on Mailjet: an MX record to the receiving
subdomain, a parseroute bound to your webhook, and a handler that
reads the JSON fields and Base64-decodes each AttachmentN.
Part 2 — The same flow on MailKite
When we built inbound into MailKite, we wanted three things to be true: one record to receive on your whole domain, a payload you don't have to reshape, and a webhook you can actually trust. Here's the same parse-a-reply flow.
1. Point your domain at MailKite
One MX record makes every address on the domain
receivable — no dedicated subdomain, no second record to prioritise:
Host Type Priority Value
yourdomain.com MX 10 mx.mailkite.dev The domain verifies as soon as that record resolves. See Domains & DNS for the SPF/DKIM/DMARC records that let you send from it too.
2. Register your webhook
Add your endpoint URL in the dashboard or via the
API and MailKite POSTs an email.received event
the moment mail arrives. Want specific addresses to hit specific endpoints?
Add a route — otherwise a catch-all sends the whole domain to one webhook.
3. Parse the payload
The message arrives already decoded — flat from,
to, subject, text, and html
fields, edge auth verdicts (SPF/DKIM/DMARC/spam), and attachments as
signed, time-limited links rather than tokens you have to redeem:
{
"id": "msg_2Hk9…",
"type": "email.received",
"from": { "address": "ada@example.com", "name": "Ada Lovelace" },
"to": [{ "address": "support@myapp.ai", "name": "Support" }],
"subject": "Re: invoice #1042",
"text": "Looks good — approved!",
"html": "<p>Looks good — approved!</p>",
"threadId": "<a1b2c3@mail.example.com>",
"receivedAt": 1785196800000,
"receivedAtIso": "2026-07-28T00:00:00.000Z",
"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=…"
}
]
} Because the URL is public, MailKite signs every request. Verify the signature over the raw body first, then handle the event — one message per call, no batch to unwrap:
import express from "express";
import { MailKite } from "mailkite";
const SECRET = process.env.MAILKITE_WEBHOOK_SECRET;
const app = express();
app.use("/hooks/mailkite", express.raw({ type: "application/json" }));
app.post("/hooks/mailkite", (req, res) => {
if (!MailKite.verifyWebhook(req.headers["x-mailkite-signature"], req.body, SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
// ...handle event.type === "email.received"
// Confirm receipt: returns the JSON body {"status":"ok"}.
res.type("application/json").send(MailKite.replyOk());
});import os
from flask import Flask, request, abort
from mailkite import verify_webhook, reply_ok
SECRET = os.environ["MAILKITE_WEBHOOK_SECRET"]
app = Flask(__name__)
@app.post("/hooks/mailkite")
def hook():
sig = request.headers.get("x-mailkite-signature", "")
if not verify_webhook(sig, request.get_data(), SECRET):
abort(401)
event = request.get_json()
# ...handle event["type"] == "email.received"
# Confirm receipt: returns the JSON body {"status":"ok"}.
return reply_ok(), 200, {"content-type": "application/json"}<?php
$mk = new \MailKite\Client(""); // no API key needed to verify
$secret = getenv("MAILKITE_WEBHOOK_SECRET");
$signature = $_SERVER["HTTP_X_MAILKITE_SIGNATURE"] ?? "";
$rawBody = file_get_contents("php://input");
if (!$mk->verifyWebhook($signature, $rawBody, $secret)) {
http_response_code(401);
exit;
}
$event = json_decode($rawBody, true);
// ...handle $event["type"] === "email.received"
header("content-type: application/json");
echo $mk->replyOk(); // {"status":"ok"}// Spring Boot
@PostMapping(value = "/hooks/mailkite", produces = "application/json")
public ResponseEntity<String> mailkite(
@RequestHeader("x-mailkite-signature") String signature,
@RequestBody byte[] rawBody) throws Exception {
MailKite mk = new MailKite(""); // no API key needed to verify
String body = new String(rawBody, StandardCharsets.UTF_8);
if (!mk.verifyWebhook(signature, body, System.getenv("MAILKITE_WEBHOOK_SECRET"))) {
return ResponseEntity.status(401).build();
}
// ...handle the event
return ResponseEntity.ok(mk.replyOk()); // {"status":"ok"}
}http.HandleFunc("/hooks/mailkite", func(w http.ResponseWriter, r *http.Request) {
rawBody, _ := io.ReadAll(r.Body)
sig := r.Header.Get("x-mailkite-signature")
if !mailkite.VerifyWebhook(sig, string(rawBody), secret) {
w.WriteHeader(http.StatusUnauthorized)
return
}
var event map[string]any
json.Unmarshal(rawBody, &event)
// ...handle event["type"] == "email.received"
w.Header().Set("content-type", "application/json")
w.Write([]byte(mailkite.ReplyOk())) // {"status":"ok"}
})require "sinatra"
require "json"
require "mailkite"
post "/hooks/mailkite" do
raw_body = request.body.read
sig = request.env["HTTP_X_MAILKITE_SIGNATURE"]
halt 401 unless Mailkite.verify_webhook(sig, raw_body, ENV["MAILKITE_WEBHOOK_SECRET"])
event = JSON.parse(raw_body)
# ...handle event["type"] == "email.received"
content_type :json
Mailkite.reply_ok # {"status":"ok"}
end
The verifyWebhook helper recomputes the HMAC and rejects forged
or stale events — see Verifying signatures
for the header format and a no-SDK version. Fields and attachment links are
documented in full under Inbound webhooks.
A little about MailKite
MailKite is programmable email for developers and AI agents. Publish one
MX record and every address on your domain becomes a signed
webhook of clean, decoded JSON — no IMAP, no polling, no MIME wrangling.
The same API sends mail, and any inbox can be handed straight to an
agent that reads and replies on its own.
Inbound and receiving are on the free tier.
Next: Inbound webhooks for routing, retries, and attachments, or the Quickstart to get a verified domain and API key.