MailKite
Get your API key
All guides Inbound email parsing

Mandrill Inbound

Inbound email processing turns the messages 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 Mandrill's inbound routes 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 Mandrill (Mailchimp Transactional) account and API key for the first half.

Part 1 — Receive inbound email with Mandrill

1. Add an inbound domain and point its MX at Mandrill

In the Mandrill dashboard, open Inbound and add the domain you want to receive on — commonly a dedicated subdomain like parse.yourdomain.com. Then add an MX record so mail sent there is delivered to Mandrill's servers instead of your own:

DNS — parse.yourdomain.com
Host                    Type   Priority   Value
parse.yourdomain.com MX 10 mandrillapp.com

DNS can take a few hours to propagate, so add this early. Back in the Inbound dashboard, use Test DNS Settings — the domain flips to MX: valid once the record resolves.

2. Add an inbound route

A route maps a mailbox pattern to a webhook URL. In the dashboard, click Routes next to your inbound domain and add one — enter a pattern (a full local part like support, or * to catch every address) and the URL Mandrill should POST to. Or do it via the API with /inbound/add-route:

add inbound route
curl -X POST https://mandrillapp.com/api/1.0/inbound/add-route \
-H "Content-Type: application/json" \
-d '{
"key": "'"$MANDRILL_API_KEY"'",
"domain": "parse.yourdomain.com",
"pattern": "*",
"url": "https://yourapp.com/hooks/mandrill"
}'

Mandrill now calls your url whenever a message matching the pattern arrives on that domain.

3. Parse the payload

This is the part to get right. Mandrill POSTs application/x-www-form-urlencodednot JSON — with a single mandrill_events field whose value is a JSON-encoded array of events. Webhooks are batched (roughly once a minute), so several messages can share one request. Each event has event: "inbound" and a msg object carrying the sender, recipients, subject, both body formats, SPF/DKIM results, a spam report, headers, and attachments:

mandrill_events (decoded)
[
{
"event": "inbound",
"ts": 1727712000,
"msg": {
"from_email": "ada@example.com",
"from_name": "Ada Lovelace",
"to": [["support@parse.yourdomain.com", "Support"]],
"email": "support@parse.yourdomain.com",
"subject": "Can't update my card",
"text": "Hi — my payment keeps failing…",
"html": "<p>Hi — my payment keeps failing…</p>",
"spf": { "result": "pass", "detail": "sender matches" },
"dkim": { "signed": true, "valid": true },
"spam_report": { "score": 0.7 },
"headers": { "Message-Id": "<abc@example.com>" },
"attachments": {
"screenshot.png": {
"name": "screenshot.png",
"type": "image/png",
"content": "iVBORw0KGgoAAAANS…",
"base64": true
}
}
}
}
]

Attachments arrive inlined as an object keyed by filename — each with type, name, content, and a base64 flag telling you whether content is Base64-encoded. Because your URL is public, verify the X-Mandrill-Signature header first: it's an HMAC-SHA1 over the webhook URL plus every POST key and value (keys sorted, concatenated with no delimiters), Base64-encoded with your webhook's key. Then JSON.parse the mandrill_events field and loop:

handle-inbound.js
import express from "express";
import crypto from "crypto";

const WEBHOOK_KEY = process.env.MANDRILL_WEBHOOK_KEY;
const WEBHOOK_URL = "https://yourapp.com/hooks/mandrill";

const app = express();
// Mandrill POSTs application/x-www-form-urlencoded, not JSON.
app.use("/hooks/mandrill", express.urlencoded({ extended: true }));

// Signature = HMAC-SHA1 over the URL + each POST key/value (keys sorted),
// concatenated with no delimiters, then Base64-encoded.
function verify(req) {
let signed = WEBHOOK_URL;
for (const key of Object.keys(req.body).sort()) {
signed += key + req.body[key];
}
const digest = crypto.createHmac("sha1", WEBHOOK_KEY).update(signed).digest("base64");
return digest === req.headers["x-mandrill-signature"];
}

app.post("/hooks/mandrill", (req, res) => {
if (!verify(req)) return res.sendStatus(401); // forged or stale — drop it.

// The mandrill_events field is a JSON-encoded array — parse it, then loop.
const events = JSON.parse(req.body.mandrill_events ?? "[]");
for (const { event, msg } of events) {
if (event !== "inbound") continue;
const body = msg.text ?? msg.html;
console.log("from", msg.from_email, "·", msg.subject);
// ...open a ticket, reply, hand to an agent.

// Attachments arrive keyed by filename, inlined as Base64:
for (const [filename, att] of Object.entries(msg.attachments ?? {})) {
const bytes = att.base64 ? Buffer.from(att.content, "base64") : Buffer.from(att.content);
// ...store ${filename} (${att.type}).
}
}
res.sendStatus(200);
});

That's inbound on Mandrill: an MX to your inbound subdomain, a route bound to a webhook, a signature check, and a handler that decodes mandrill_events, walks the batch, and un-Base64s each inlined attachment.

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:

DNS — yourdomain.com
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 Base64 blobs you have to decode. It's a clean single event, not a form-encoded array you must un-nest:

POST /hooks/mailkite
{
"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:

handle-inbound
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());
});
Install Docs →

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.