Email to JSON: MailParse vs SendGrid Inbound Parse

Compare MailParse and SendGrid Inbound Parse for Email to JSON. Features, performance, and developer experience.

Why Email to JSON Matters for Developers

Email-to-JSON conversion sits at the heart of many SaaS workflows. Support ticketing, lead capture, vendor integrations, and automated approvals all depend on taking raw MIME messages and converting them into clean, predictable JSON that your application can consume. Getting this right reduces brittle parsing code, eliminates edge-case bugs, and ensures every message - plaintext, HTML, or multi-part with inline images and attachments - becomes a well-structured payload.

When choosing between a purpose-built email-to-JSON platform and a sending provider's inbound feature, you are weighing developer experience and parsing fidelity against convenience if you are already in that ecosystem. This comparison focuses exclusively on how each approach delivers JSON for application consumption, not on outbound sending or marketing features.

How MailParse Handles Email to JSON

This platform is designed to receive inbound email on instant addresses, parse raw MIME reliably, and deliver normalized JSON via webhook or make it available for REST polling. The pipeline focuses on canonical structure and full fidelity so downstream code is simple and deterministic.

Webhook delivery

  • Content type: application/json
  • HMAC signature header for request verification
  • Automatic retries with backoff on non-2xx responses, with an idempotency key
  • Consistent schema across all message shapes, including multi-part, digests, and TNEF

REST polling

  • List and fetch messages with filters by recipient address, timestamp, and processing status
  • Attachment streaming endpoints for large files to avoid memory pressure

JSON schema highlights

The JSON payload is normalized so you do not need to guess at where fields live or what type they are. Typical structure:

{
  "id": "msg_01HX9W8YB3KZPJJ7N2VF9QZ80A",
  "received_at": "2026-04-23T12:31:08.123Z",
  "envelope": {
    "from": {"address": "alice@example.com", "name": "Alice"},
    "to": [{"address": "support@yourapp.com", "name": ""}],
    "cc": [],
    "bcc": [],
    "reply_to": [{"address": "alice+reply@example.com", "name": "Alice"}],
    "return_path": "bounces@example.com",
    "subject": "Bug report with screenshots",
    "message_id": "<9caf1b84-2a7f-4f54-bd13-b6@mailer.example>",
    "in_reply_to": null,
    "references": []
  },
  "headers": {
    "dkim-signature": "...",
    "authentication-results": "spf=pass dkim=pass dmarc=pass",
    "x-priority": "3"
  },
  "auth": {
    "spf": {"result": "pass", "domain": "example.com"},
    "dkim": [{"result": "pass", "domain": "example.com"}],
    "dmarc": {"result": "pass", "policy": "reject", "alignment": "strict"},
    "arc": {"sealed": true, "chain_result": "pass"}
  },
  "body": {
    "text": "Hi team,\nSee the attached screenshots.\nThanks,\nAlice",
    "html": "<p>Hi team,</p><p>See the attached screenshots.</p><p>Thanks,<br/>Alice</p>"
  },
  "attachments": [
    {
      "id": "att_01H...1",
      "filename": "screenshot1.png",
      "mime_type": "image/png",
      "size": 182344,
      "content_id": "image001@local",
      "disposition": "inline",
      "sha256": "6fbd...af",
      "download_url": "https://attachments.your-service.com/att_01H...1",
      "is_inline": true
    },
    {
      "id": "att_01H...2",
      "filename": "log.txt",
      "mime_type": "text/plain",
      "size": 5325,
      "disposition": "attachment",
      "sha256": "1a35...91",
      "download_url": "https://attachments.your-service.com/att_01H...2",
      "is_inline": false
    }
  ],
  "inline_map": {
    "image001@local": "att_01H...1"
  },
  "spam": {
    "score": 0.1,
    "report": "..."
  },
  "raw": {
    "mime_id": "raw_01H...",
    "size": 244832,
    "download_url": "https://raw.your-service.com/raw_01H..."
  },
  "meta": {
    "charset": "utf-8",
    "decoded": true
  }
}

Key developer benefits:

  • No multipart parsing burden in your app
  • All addresses normalized into objects with name and address
  • Inline images linked to attachments via content-id map for easy HTML rewriting
  • Headers are case-insensitive and consistently keyed
  • Raw MIME and attachment streaming available for audit or reprocessing

For related implementation checklists, see the Email Infrastructure Checklist for SaaS Platforms and Top Inbound Email Processing Ideas for SaaS Platforms.

How SendGrid Inbound Parse Handles Email to JSON

Twilio SendGrid's Inbound Parse posts incoming messages to your HTTPS endpoint as multipart/form-data. Fields like from, to, subject, text, and html arrive as form fields. Attachments are posted as uploaded files (attachment1, attachment2, etc.) with an attachment-info field that includes per-attachment metadata as JSON. There is also a charsets field carrying character set hints as a JSON string, and an envelope field containing SMTP envelope details as a JSON string.

Important characteristics for email-to-JSON workflows:

  • Payload format is multipart rather than JSON. You must parse and normalize into your own schema.
  • Headers are provided as a single raw string in the headers field. You need to split and case-normalize.
  • Attachments arrive as individual files in the form payload. You are responsible for streaming them to storage and calculating hashes if needed.
  • An optional setting can include the full raw message. If enabled, the MIME is delivered in an email part that you must parse yourself if you want rich structure.
  • Authentication context such as DKIM, SPF, and spam scoring can be provided as fields like dkim, SPF, spam_report, and spam_score. Expect strings that you normalize and typecast.
  • Security: inbound parse does not include a built-in request signature. Best practice is to use IP allowlists, TLS, and optional Basic Auth on your endpoint.
  • Setup requires pointing MX records for your domain to SendGrid. This ties inbound to the SendGrid ecosystem, which is ideal if you already send there but can be limiting if you are multi-provider.

In short, sendgrid-inbound-parse is capable and battle-tested, but it leaves the email-to-JSON conversion step up to your code. Many teams wrap it with a normalization layer before handing payloads to application logic.

Side-by-Side Comparison of Email-to-JSON Features

Feature MailParse SendGrid Inbound Parse
Webhook payload format application/json with canonical schema multipart/form-data, fields and files
Raw MIME availability Always available with download URL Available if you enable raw MIME option
Attachments Array with metadata, streaming download endpoints, SHA-256 Uploaded files in request, metadata in attachment-info JSON
Inline images mapping content-id to attachment id map included No direct map - you compute from headers
Address normalization Objects for from, to, cc, bcc, reply-to with name and address Comma-delimited strings - you parse and normalize
Auth results (SPF/DKIM/DMARC/ARC) Structured objects Fields, often as strings, require normalization
Character set handling Decoded to UTF-8, original charset retained in meta charsets provided as JSON string - you re-encode
Spam scoring Score and report fields spam_score and spam_report strings
Webhook verification HMAC signature header with shared secret Use TLS, IP allowlist, optional Basic Auth
Retries and idempotency Managed retries with idempotency key Rely on your endpoint logic and SendGrid retry behavior
REST polling option Yes, with filtering and attachment streaming No, webhook only
Setup complexity Instant addresses or custom domain with simple DNS Requires MX changes into SendGrid ecosystem

Code Examples

Receiving JSON webhook from MailParse

Example Node.js endpoint that verifies the HMAC signature and handles a normalized payload:

import crypto from "crypto";
import express from "express";

const app = express();
app.use(express.json({ limit: "25mb" }));

const SHARED_SECRET = process.env.WEBHOOK_SECRET;

function verifySignature(req) {
  const sig = req.header("X-Signature");
  const timestamp = req.header("X-Timestamp");
  const body = JSON.stringify(req.body);
  const hmac = crypto
    .createHmac("sha256", SHARED_SECRET)
    .update(`${timestamp}.${body}`)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(hmac, "hex"));
}

app.post("/inbound/webhook", (req, res) => {
  if (!verifySignature(req)) {
    return res.status(401).send("invalid signature");
  }

  const msg = req.body;

  // Access normalized fields
  const subject = msg.envelope.subject;
  const from = msg.envelope.from.address;
  const text = msg.body.text || "";
  const html = msg.body.html || "";

  // Inline image rewriting example
  if (html && msg.inline_map) {
    Object.entries(msg.inline_map).forEach(([cid, attId]) => {
      const attachment = msg.attachments.find(a => a.id === attId);
      if (attachment) {
        const url = attachment.download_url;
        // replace cid: links with public URLs
        // html = html.replace(new RegExp(`cid:${cid}`, "g"), url);
      }
    });
  }

  // Persist or enqueue for downstream processing
  // db.storeMessage(msg);

  res.sendStatus(200);
});

app.listen(3000, () => console.log("listening on :3000"));

Receiving sendgrid-inbound-parse and converting to JSON

Example Node.js endpoint using multer to handle multipart, then normalizing into a structured shape that looks like a canonical email-to-JSON object:

import express from "express";
import multer from "multer";

const app = express();
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });

app.post("/sendgrid/inbound", upload.any(), async (req, res) => {
  // SendGrid posts form fields and files
  const fields = Object.fromEntries(Object.entries(req.body).map(([k, v]) => [k, Array.isArray(v) ? v[0] : v]));
  const files = req.files || [];

  // Basic normalization helpers
  const parseAddresses = str => {
    if (!str) return [];
    return str.split(",").map(s => {
      const m = s.trim().match(/^(?:"?([^"]*)"?\s)?<(.+@.+)>$/);
      return m ? { name: m[1] || "", address: m[2] } : { name: "", address: s.trim() };
    });
  };

  const attachmentInfo = fields["attachment-info"] ? JSON.parse(fields["attachment-info"]) : {};

  const normalized = {
    received_at: new Date().toISOString(),
    envelope: {
      from: parseAddresses(fields.from)[0] || { name: "", address: "" },
      to: parseAddresses(fields.to),
      cc: parseAddresses(fields.cc),
      bcc: parseAddresses(fields.bcc),
      reply_to: parseAddresses(fields["reply-to"]),
      return_path: "", // Not provided directly
      subject: fields.subject || "",
      message_id: "", // Derive from headers if needed
      in_reply_to: null,
      references: []
    },
    headers: (() => {
      const raw = fields.headers || "";
      const out = {};
      raw.split(/\r?\n/).forEach(line => {
        const idx = line.indexOf(":");
        if (idx > 0) {
          const key = line.slice(0, idx).toLowerCase();
          const val = line.slice(idx + 1).trim();
          if (key) out[key] = val;
        }
      });
      return out;
    })(),
    auth: {
      spf: { result: (fields.SPF || "").toLowerCase(), domain: "" },
      dkim: [{ result: (fields.dkim || "").toLowerCase(), domain: "" }],
      dmarc: { result: "", policy: "", alignment: "" },
      arc: { sealed: false, chain_result: "" }
    },
    body: {
      text: fields.text || "",
      html: fields.html || ""
    },
    attachments: files
      .filter(f => f.fieldname.startsWith("attachment"))
      .map(f => {
        const meta = attachmentInfo[f.originalname] || {};
        return {
          id: `att_${f.fieldname}`,
          filename: f.originalname,
          mime_type: f.mimetype,
          size: f.size,
          content_id: meta.content_id || null,
          disposition: meta.disposition || "attachment",
          sha256: "", // compute if needed
          download_url: "", // upload to your storage and set
          is_inline: meta.disposition === "inline"
        };
      }),
    inline_map: {}, // derive from attachments with content_id
    spam: {
      score: fields.spam_score ? parseFloat(fields.spam_score) : null,
      report: fields.spam_report || ""
    },
    raw: {
      mime_id: "",
      size: fields.email ? Buffer.byteLength(fields.email, "utf8") : 0,
      download_url: "" // upload raw if you store it
    },
    meta: {
      charset: (() => {
        try {
          const ch = fields.charsets ? JSON.parse(fields.charsets) : {};
          return ch.html || ch.text || "utf-8";
        } catch { return "utf-8"; }
      })(),
      decoded: true
    }
  };

  // If html contains cid: links, you can populate inline_map
  normalized.attachments.forEach(a => {
    if (a.content_id) normalized.inline_map[a.content_id] = a.id;
  });

  // Persist normalized message for your app
  // db.storeMessage(normalized);

  res.sendStatus(200);
});

app.listen(3001, () => console.log("SendGrid inbound listening on :3001"));

By fronting sendgrid's payload with a small normalization layer like the above, you get a stable email-to-JSON contract for the rest of your application.

Performance and Reliability

Large attachments and streaming

Multipart webhook posts can stress your application server if you do not stream uploads to disk or object storage. The multipart approach from Twilio requires careful memory limits and backpressure handling. A dedicated email-to-JSON service that stores attachments and exposes streaming download URLs removes that pressure and makes your webhook fast.

Character sets and decoding

Email bodies arrive in many encodings, not just UTF-8. Some providers expose a charsets hint, but you are responsible for re-encoding and joining multipart alternatives. A normalized JSON payload that is always UTF-8 with original charset retained in metadata simplifies search indexing and downstream processing.

Edge cases

  • TNEF winmail.dat: Properly extracting original attachments and rich text avoids data loss.
  • S/MIME and PGP: Messages may be signed or encrypted. Exposing signature state and preserving the raw MIME is essential for audit.
  • Inline images: Accurate mapping of Content-ID to attachments avoids broken images when rendering HTML in your product.
  • Weird headers: Folding, RFC 2231 filenames, and non-ASCII names require robust decoding and normalization.

Security and authenticity

A JSON webhook with an HMAC signature header provides a straightforward way to verify that the payload really came from your provider. With sendgrid-inbound-parse, you typically combine TLS, IP allowlists, and optional Basic Auth to secure your endpoint. In both cases you should also record the original authentication results from the sender (SPF/DKIM/DMARC) and use them in your business logic.

Hardening your approach end to end is easier when you pair a JSON-first payload with explicit verification. For broader operational checklists, see the Email Deliverability Checklist for SaaS Platforms and Top Email Parsing API Ideas for SaaS Platforms.

Verdict: Which Is Better for Email to JSON?

If your primary requirement is converting email into high-fidelity JSON with minimal custom code, MailParse delivers a cleaner developer experience. Webhooks arrive as canonical JSON, attachments are offloaded with stable URLs, and inline content is mapped for you. You get predictable fields for headers, authentication results, and spam scoring without additional parsing.

If you already use Twilio for sending and prefer to keep everything in one ecosystem, SendGrid Inbound Parse is reliable and widely deployed. Expect to write and maintain a normalization layer that turns multipart form posts into a schema your application trusts. For many teams, this layer is a small one-time investment. For others, especially those facing diverse inbound sources and formats, a JSON-first approach reduces ongoing complexity.

FAQ

What are the biggest differences between JSON webhooks and multipart form posts for email-to-JSON?

JSON webhooks remove the need to parse fields and files at the edge. You can verify a signature, deserialize once, and move on. Multipart form posts require a streaming parser, separate handling for attachments, and custom normalization for addresses, headers, and auth results. Both models can deliver complete data, but JSON reduces boilerplate and surface area for bugs.

How should I handle attachments with sendgrid inbound parse?

Use a streaming multipart parser with strict memory limits. For each attachmentN file, upload to object storage and persist metadata from attachment-info. Compute a content hash, keep the original filename and disposition, and build a content-id map if you want to rewrite HTML cid links to your own URLs. Avoid buffering large files in memory.

How do I validate that inbound webhooks are authentic?

Prefer request signatures with a shared secret when available, and verify them on every request. If your provider does not sign inbound webhooks, enable TLS, limit accepted source IP ranges, and add Basic Auth on the endpoint. Also store sender-side SPF and DKIM results from the message and use them in any trust decisions.

Can I start with SendGrid and later switch to a JSON-first service?

Yes. Abstract your application behind a small normalization layer that converts sendgrid's multipart payload to your canonical JSON shape. When you switch, point your webhook to the new provider and keep the canonical schema intact, minimizing downstream changes.

What about multi-tenant SaaS use cases?

For multi-tenant systems, prefer stable JSON schemas, HMAC-signed webhooks, and attachment URLs that you can scope to tenants. Determine routing by recipient address and include per-tenant secrets for signature verification. Make sure your design accounts for replay protection and idempotency when webhooks retry.

Ready to get started?

Start parsing inbound emails with MailParse today.

Get Started Free