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
headersfield. 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
emailpart 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, andspam_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.