MailKite
Get your API key
All guides Inbound email parsing

SendGrid Inbound Parse

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 SendGrid's Inbound Parse webhook 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 SendGrid account for the first half.

Part 1 — Parse inbound email with SendGrid

1. Point a subdomain at SendGrid

Inbound Parse works on a dedicated receiving subdomain — commonly parse.yourdomain.com. Add one MX record so mail sent there is delivered to SendGrid's servers instead of your own:

DNS — parse.yourdomain.com
Host              Type   Priority   Value
parse.yourdomain.com MX 10 mx.sendgrid.net

DNS can take a few hours to propagate, so add this early. Any address on that subdomain — support@parse.yourdomain.com, ada@parse.yourdomain.com — will now flow into SendGrid.

2. Configure the Inbound Parse setting

In the SendGrid dashboard, go to Settings → Inbound Parse and add a host. You provide two things: the receiving domain (the subdomain whose MX you just pointed, e.g. parse.yourdomain.com) and the destination URL — the public endpoint SendGrid should POST parsed mail to. Optional toggles here let you check incoming mail for spam or receive the raw, full MIME message instead of the parsed fields.

SendGrid now POSTs to your URL every time a message arrives on that subdomain.

3. Parse the payload

Here's the important part: SendGrid does not send JSON. It POSTs multipart/form-data, with each piece of the email as a separate form field. The default fields include the sender, recipients, subject, both body formats, the raw headers, the SMTP envelope and charsets as JSON strings, SPF/DKIM results, an attachments count, and the files themselves:

form fields — POST /hooks/sendgrid
from            Ada Lovelace <ada@example.com>
to support@parse.yourdomain.com
subject Can't update my card
text Hi — my payment keeps failing…
html <p>Hi — my payment keeps failing…</p>
headers (raw RFC 822 headers of the message)
envelope {"to":["support@parse.yourdomain.com"],"from":"ada@example.com"}
charsets {"to":"UTF-8","subject":"UTF-8","text":"UTF-8",…}
SPF pass
dkim {@example.com : pass}
sender_ip 198.51.100.24
attachments 1
attachment-info {"attachment1":{"filename":"screenshot.png","type":"image/png",…}}
attachment1 (the file itself, as a multipart part)
spam_score 0.7 # only if "Check for spam" is enabled
spam_report … # only if "Check for spam" is enabled

Attachments arrive as multipart file parts named attachment1, attachment2, and so on, with their filenames and types described in the attachment-info JSON string. Use a multipart parser — multer, busboy, or formidable — to read the text fields and the files:

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

const app = express();
// SendGrid POSTs multipart/form-data, not JSON. Parse the text fields,
// and keep uploaded files in memory (or stream them to disk / object storage).
const upload = multer({ storage: multer.memoryStorage() });

app.post("/hooks/sendgrid", upload.any(), (req, res) => {
const { from, to, subject, text, html } = req.body;
console.log("from", from, "·", subject);
// ...open a ticket, reply, hand to an agent.

// envelope, charsets and attachment-info arrive as JSON *strings* — parse them.
const info = JSON.parse(req.body["attachment-info"] ?? "{}");

// Files come through as multipart parts (attachment1, attachment2, …),
// surfaced by multer on req.files:
for (const file of req.files ?? []) {
// file.originalname, file.mimetype, file.buffer
}
res.sendStatus(200);
});

That's inbound parsing on SendGrid: an MX record to the receiving subdomain, an Inbound Parse host pointed at your URL, and a handler that reads multipart/form-data fields and pulls attachments off the multipart parts. Remember that envelope, charsets, and attachment-info are JSON strings you have to parse, and that spam_score only appears when spam checking is enabled.

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 multipart parts you have to buffer:

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 multipart to unpack:

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.