MailKite
Get started
All posts
Gabe 5 min read

The honest SendGrid Inbound Parse alternative

SendGrid Inbound Parse POSTs your inbound mail as multipart/form-data and leaves the parsing, decoding, and attachment handling to you — with a well-earned reputation for encoding and attachment mangling. Here's a fair comparison, what MailKite does differently (fully parsed JSON, auth results, signed attachment URLs, no daily cap), and what it doesn't.

SendGrid Inbound Parse is Twilio SendGrid’s inbound feature: it accepts mail sent to a domain or subdomain you’ve pointed at SendGrid via MX, and POSTs the message to your endpoint as multipart/form-data — a form upload with the headers, body text, and attachments as separate fields you parse yourself. MailKite is a parsed-inbound alternative: the same inbound mail arrives as a single JSON webhook with decoded text/html, SPF/DKIM/DMARC results, and attachments as short-lived signed URLs — no form-parsing, no charset guessing, and no daily send cap.

I want to be fair to SendGrid, because Inbound Parse is a real, widely-used feature and this is a comparison post, not a hit piece. SendGrid moves enormous volumes of mail reliably, Inbound Parse has existed for years, and for a simple “email us and we’ll POST it to a script” use case it works. If your needs are that modest, it’s a reasonable choice and you don’t need us.

This post is for when the modest case grows teeth.

What Inbound Parse actually gives you

Point your MX at mx.sendgrid.net, register a host in the Inbound Parse settings, and mail to that domain gets POSTed to your URL as multipart/form-data. You get fields like from, subject, text, html, a headers blob, and — if you tick the box — attachments as file parts plus a charsets field describing how each text field was encoded.

That last detail is the tell. The reason there’s a charsets field at all is that decoding is your job: SendGrid hands you bytes and a label, and you’re expected to re-decode each field into UTF-8 yourself. Miss it and you get the canonical bug — a £ arriving as £, an emoji as mojibake — because the field was latin-1 or ISO-2022-JP and you read it as UTF-8. This is the single most-reported Inbound Parse pain, and it’s structural: the format makes correct decoding opt-in.

Attachments compound it. They arrive as multipart file parts you extract from the request body, and developers repeatedly report attachments arriving mislabeled, re-encoded, or corrupt — filenames with the wrong charset, content types that don’t match, files that don’t round-trip. Again: not because SendGrid is careless, but because “parse a multipart form and re-decode every part” is handed back to you, and there are a hundred ways to get it subtly wrong.

And there’s one more edge that bites in production: if your endpoint returns a 4xx/5xx or has a DNS hiccup when Parse tries to POST, the message can be dropped rather than retried. Inbound you never see is the worst kind.

The honest comparison

No adjective inflation — here’s the shape of the difference:

SendGrid Inbound ParseMailKite inbound
Payload formatmultipart/form-data you parseSingle JSON webhook
Body decodingYou re-decode per charsets fieldtext/html pre-decoded to UTF-8
AttachmentsMultipart file parts, inlineMetadata + short-lived signed url
Sender authYou infer from raw headersauth{spf,dkim,dmarc,spam} in payload
On endpoint errorCan drop with no retryAutomatic retries
Free tierSendGrid’s free tier changed in 20253,000 msgs/mo, no daily cap
Still parse-work on topYes — form + decode + attachmentsNo — read the fields

The through-line: with Inbound Parse you own the form-parsing, the re-decoding, the attachment extraction, and the auth inference. With MailKite that work is done before the webhook reaches you.

What the JSON looks like

Same inbound email, delivered parsed:

{
  "id": "msg_2Hk9…",
  "type": "email.received",
  "from": { "address": "ada@example.com" },
  "to": [{ "address": "support@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=…" }
  ]
}

No charsets field to reconcile — text and html are already UTF-8, so the £ is a £. Attachments are out of the payload as signed URLs. And auth is right there, which matters the moment you act on an email: a forged From: is an authorization decision, and having SPF/DKIM/DMARC in the payload means you don’t infer trust from raw headers or trust blindly.

The handler

// Express
import express from "express";
import { MailKite } from "mailkite";

const SECRET = process.env.MAILKITE_WEBHOOK_SECRET;

// Verify the RAW bytes — re-serializing breaks the HMAC.
app.use("/hooks/mailkite", express.raw({ type: "application/json" }));

app.post("/hooks/mailkite", (req, res) => {
  const sig = req.headers["x-mailkite-signature"];
  if (!MailKite.verifyWebhook(sig, req.body, SECRET)) return res.sendStatus(401);

  const event = JSON.parse(req.body);
  if (event.type === "email.received" && event.auth.spf === "pass") {
    console.log(event.from.address, "·", event.subject);
    // event.text / event.html already decoded; attachments are signed URLs.
  }

  res.sendStatus(200); // ack fast; heavy work out of band
});

app.listen(3000);

No multipart parser, no charsets reconciliation, no attachment extraction — verify the signature, read the fields. The same handler exists for Python, Ruby, Go, PHP, and Java; see the receiving docs and webhook security.

Where I won’t overclaim

SendGrid is a mature, high-scale sending platform with deliverability tooling and a track record we’re not going to pretend to match on day one. If you’re already deep in SendGrid for outbound and Inbound Parse is a minor side feature, switching purely for inbound may not be worth it. Our claim is narrower and specific: for the inbound direction — parsing, decoding, attachments, auth, and retries — MailKite hands you a finished JSON message instead of a form to parse. That’s the pain Inbound Parse is famous for, and it’s the one we built for.

FAQ

What is SendGrid Inbound Parse, exactly? It’s SendGrid’s inbound feature: you MX a domain to SendGrid, and mail to it is POSTed to your endpoint as multipart/form-data with the body and attachments as separate fields you parse and re-decode yourself.

Why do attachments and special characters get mangled with Inbound Parse? Because decoding is opt-in on your side. Text fields carry a charsets map you must honor to re-decode to UTF-8, and attachments come as multipart parts you extract — both are easy to get subtly wrong, which produces £-style corruption and mislabeled files.

What’s the main difference with MailKite? MailKite delivers the message as parsed JSON — text/html already decoded, auth{spf,dkim,dmarc,spam} included, and attachments as short-lived signed URLs — with automatic retries on delivery failure, so there’s no form-parsing or re-decoding left for you.

Is there a daily sending cap? No. The free tier is 3,000 messages a month across inbound and outbound with no daily cap, and metered overage instead of a hard cutoff — a common friction point when free tiers change.

Should everyone switch off Inbound Parse? No. If Inbound Parse meets your needs and you’re invested in SendGrid for sending, stay. Switch when the parsing, decoding, attachment, and auth work has become your problem to maintain.


If Inbound Parse has you writing form parsers and chasing £ bugs, there’s a cleaner shape. Point a domain at MailKite and your next inbound email arrives as parsed JSON.

Related: Receiving email is the part nobody warns you about makes the full case for inbound, handling attachments without losing the £ sign goes deep on the encoding bug, and Cloudflare Email Routing can’t reply is the same comparison for Cloudflare.

Discuss this post: Hacker News Share on X

Related posts