The best inbound email parsing API isn't the most popular one — it's the one that drops a clean, structured payload on your webhook without you spending two days writing MIME parsers. Four credible options exist: Mailgun Inbound, SendGrid Inbound Parse, Postmark Inbound, and Mails.ai. They all receive email and fire a webhook. The differences — payload structure, latency, classification, threading support, pricing — determine which one actually fits your production system.
This guide makes the call for each use case.
What inbound email parsing actually does
An inbound email parsing API sits between the internet's mail infrastructure and your application code. It accepts SMTP connections at an MX record you control, processes the raw RFC 5322 message, extracts structured fields, and delivers everything to your webhook as JSON — so your code never touches raw MIME.
The MX record is the entry point. You point mail.yourdomain.com MX 10 inbound.provider.com and every message to that subdomain lands at the provider's ingestion infrastructure. From there:
- SMTP reception — the provider's MTAs accept the connection, perform SPF/DKIM verification on the sender, and queue the message.
- MIME parsing — the multipart body is decoded:
text/plain,text/html, base64-decoded attachments, inline images. - Header extraction —
From,To,Cc,Subject,Message-ID,In-Reply-To,Referencesare parsed into discrete fields. - Webhook delivery — the provider POSTs a JSON payload (or, in some cases, multipart form data) to your endpoint with a configurable retry policy.
What does that JSON look like, and how much work is left after you receive it?
sequenceDiagram
participant Sender
participant MX as Provider MX
participant Parser as MIME Parser
participant WH as Your Webhook
Sender->>MX: SMTP MAIL FROM RCPT TO DATA
MX->>Parser: Raw RFC5322 message
Parser->>Parser: Extract headers attachments body
Parser->>WH: POST JSON payload
WH->>WH: Route classify act
The four contenders: architecture comparison
Mailgun Inbound Routes
Mailgun's inbound pipeline is built around Routes — regex or header-match rules that trigger actions (forward, store, webhook). The webhook payload is multipart/form-data, not JSON. That matters: your endpoint has to parse a form body, then parse the body-mime field (a raw MIME string) if you want attachment access. Attachments arrive as separate form fields.
Payload structure (simplified):
recipient=you@domain.com
sender=them@external.com
subject=Re: Order 4821
body-plain=Thanks for the update...
body-html=<html>...
message-headers=[["Received",...],["Message-Id","<abc@mail.gmail.com>"],...]
attachment-count=1
attachment-1=<binary file>
Message-ID and In-Reply-To are buried inside the message-headers JSON array — you have to iterate it. For threading in agent workflows, that's real friction. Mailgun verifies SPF/DKIM and exposes results in the headers array, but doesn't surface them as top-level fields.
The route matching is flexible for sending to multiple endpoints, but each route is a separate configuration object. At scale with many agent mailboxes, that becomes a management problem.
Pricing: Inbound parsing is bundled into sending plans. The Flex plan charges $0.80/1,000 messages sent, with inbound processing included. For pure inbound-heavy workloads — agents receiving thousands of replies — you're paying for sending capacity you don't use.
SendGrid Inbound Parse
SendGrid's Inbound Parse posts multipart/form-data with a structure similar to Mailgun. The headers field is a raw string, not parsed — you get Message-ID: <abc123@mail.gmail.com>\r\nFrom: ... and you parse it yourself. Attachments are separate form fields.
SPF results appear in a dkim field and a SPF field as plain strings (pass, fail). Threading fields (In-Reply-To, References) are only accessible by parsing the raw headers string.
SendGrid Inbound Parse has a known architectural limitation: it doesn't enforce TLS on inbound connections the way dedicated inbound APIs do. For security-sensitive workflows, that matters.
Inbound parse is configured per-domain, not per-address. Every email to *@yourdomain.com hits the same webhook. Routing by recipient means parsing To or envelope yourself.
Pricing: Free on paid plans. Essentials starts at $19.95/month for 50K emails. No per-inbound pricing — and no granular inbound tooling either.
Postmark Inbound
Postmark's inbound processing is the cleanest of the traditional providers. The webhook is JSON, not form-data, and the payload is well-normalized:
{
"From": "sender@example.com",
"FromName": "Alice",
"To": "inbox@yourdomain.com",
"Subject": "Re: Support ticket 1042",
"MessageID": "<abc123@mail.example.com>",
"ReplyTo": "",
"MailboxHash": "ticket-1042",
"TextBody": "Thanks for your help...",
"HtmlBody": "<html>...",
"StrippedTextReply": "Thanks for your help...",
"Headers": [
{ "Name": "In-Reply-To", "Value": "<original@mail.example.com>" }
],
"Attachments": [...]
}
The MailboxHash is useful: if you send from reply+{hash}@yourdomain.com, Postmark extracts the hash into its own field. That's the standard pattern for correlating replies to a context — ticket ID, agent session ID, order number. StrippedTextReply attempts to remove quoted reply text. Useful, not always accurate.
Threading fields (In-Reply-To, References) live in the Headers array — one step better than raw strings, but still not top-level. No built-in classification, no content-based routing.
Pricing: $1.25/1,000 messages, sent or received combined. At 10,000 inbound messages/month, that's $12.50. Clean and predictable, but blended with sending volume.
Mails.ai Inbound
Mails.ai's inbound parsing was built for agents and automated systems — not bolted onto a sending API as an afterthought, but designed as a first-class inbound pipeline. The differences show up in the payload and in what happens before the webhook fires.
Payload structure (a Mails.ai reply.received webhook, trimmed):
{
"id": "evt_01K…",
"type": "reply.received",
"workspace_id": "wsk_01K…",
"agent_id": "agt_01K…",
"thread_id": "thrd_01K…",
"source_message_id": "rcv_01K…",
"intent": "general_reply",
"injection_score": 0.02,
"sender_reputation": 0.85,
"quarantined": false,
"test_mode": false,
"created_at": "2026-08-18T09:14:34Z",
"data": {
"from": { "address": "user@example.com", "name": "Alice" },
"subject": "Re: Your report is ready",
"extracted_text": "Looks good, please proceed.",
"in_reply_to_message_id": "msg_01K…",
"is_thread_reply": true,
"dkim": "pass",
"dkim_domain": "example.com",
"dmarc": "pass",
"spf": "unknown",
"has_attachment": true,
"attachments": [
{ "id": "att_01K…", "filename": "report.pdf", "content_type": "application/pdf", "size": 84210 }
],
"raw_url": "/api/v1/messages/rcv_01K…/raw",
"received_at": "2026-08-18T09:14:33Z"
}
}
The reply arrives already matched to its thread: thread_id names the conversation, and in_reply_to_message_id is the message of yours it answers, so there are no headers to parse. extracted_text is the reply with the quoted history stripped. The sender's authentication sits in data, as Mails.ai checked it from the email itself: dkim, dkim_domain and dmarc. Every inbound within the plan's limits is scanned for prompt injection: injection_score is the evidence and quarantined the verdict.
A reply's intent is always general_reply. Mails.ai's opt-in classification reads first-contact mail instead (the message.received event) and adds a free-form intent, the entities it found and an urgency score before the webhook fires, so your handler can branch on a new request without an LLM call per message.
That matters when a human reply of "yes, go ahead" needs to resume the right agent run: thread_id tells you which one, and extracted_text hands your model only the new words.
Mails.ai also exposes a Model Context Protocol server (@mailsai/mcp-server on npm), so LLM-based agents can call mails_list_replies, mails_reply, and mails_get_thread as native MCP tools — no custom webhook infrastructure needed.
Pricing: flat plans with a separate inbound allowance. Free covers 3,000 emails + 3,000 inbound a month; Pro is $20/month for 50,000 emails + 50,000 inbound (delivery + injection scan included). Classification is opt-in on paid plans at no extra charge. At 10,000 inbound messages/month: $20 on Pro, with or without classification. No seat fee.
Feature comparison table
| Feature | Mailgun | SendGrid | Postmark | Mails.ai |
|---|---|---|---|---|
| Webhook format | multipart/form | multipart/form | JSON | JSON |
Message-ID top-level |
❌ (in array) | ❌ (raw string) | ✅ | Its own id, source_message_id |
In-Reply-To top-level |
❌ | ❌ | ❌ (in array) | Resolved to thread_id |
| SPF/DKIM top-level | ❌ | Partial | ❌ | In data: spf, dkim, dmarc |
| Stripped reply body | ❌ | ❌ | ✅ | ✅ |
| Built-in classification | ❌ | ❌ | ❌ | ✅ (opt-in) |
| MCP-native tools | ❌ | ❌ | ❌ | ✅ |
| Per-address routing | Via routes | Domain-level only | Via hash | Per-address |
| Inbound pricing | Bundled | Bundled | Blended/1K | Own allowance (50K on $20 Pro) |
| TypeScript SDK | ❌ | ❌ | Community | @mailsai/sdk |
When to use each
Choose Mailgun if you're already deep in its ecosystem for sending and need basic inbound routing to a few endpoints. The route matching is flexible. Don't choose it if you need clean threading data — you'll spend real engineering time on header extraction.
Choose SendGrid Inbound Parse if your team already uses SendGrid for transactional sending and your inbound volume is low. The domain-level routing is a genuine limitation for multi-tenant or multi-agent setups.
Choose Postmark if you need a clean JSON webhook and a straightforward use case: support ticket ingestion, contact form replies, notification confirmations. The MailboxHash pattern works well. It's not built for agents — no classification, no MCP — but it's reliable and well-documented.
Choose Mails.ai if:
- You're building an AI agent that reads and acts on email replies
- You want each reply matched to its thread (
thread_id) without writing a header parser - You want classification to branch agent logic without an extra LLM call per message
- Your architecture is MCP-native or you're building on an LLM framework (LangGraph, CrewAI, AutoGen)
- You want inbound on its own allowance, with a free tier for idle or low-volume agents
The Mails.ai API exposes inbound configuration, webhook management, and address provisioning through a REST API with a TypeScript SDK (npm install @mailsai/sdk).
Implementing inbound parsing with Mails.ai
Here's what a production inbound handler looks like with the TypeScript SDK:
import express from 'express';
import { mails } from '@mailsai/sdk';
const app = express();
// Express handler (same pattern works for Next.js API routes, Hono, etc.)
// The signature covers the raw body, so read the body raw.
app.post('/webhooks/inbound', express.raw({ type: 'application/json' }), async (req, res) => {
// Verify the webhook signature (HMAC-SHA256)
const event = mails.webhooks.verify(
req.body.toString('utf8'),
req.header('x-mails-signature') ?? '',
process.env.MAILS_WEBHOOK_SECRET!,
);
if (!event) return res.sendStatus(401);
if (event.type === 'reply.received') {
// Our verdict on prompt injection: don't hand a flagged reply to the model
if (event.quarantined) return res.sendStatus(200);
// Threading — no header parsing needed
await resumeAgentWorkflow(event.thread_id, String(event.data.extracted_text ?? ''));
} else if (event.type === 'message.received') {
// Branch on classification without an extra LLM call
if (event.intent === 'schedule_meeting') await bookMeeting(event.entities);
else await routeToHumanQueue(event);
}
res.sendStatus(200);
});
For Python backends, the mailsai package on PyPI checks the signature the same way:
import os
from flask import Flask, request, abort
from mailsai import verify_webhook
app = Flask(__name__)
@app.post('/webhooks/inbound')
def inbound():
event = verify_webhook(
request.get_data(as_text=True),
request.headers.get('X-Mails-Signature', ''),
os.environ['MAILS_WEBHOOK_SECRET'],
)
if event is None:
abort(401)
if event['type'] == 'reply.received' and not event.get('quarantined'):
thread_id = event['thread_id']
text = event['data']['extracted_text']
# ... route to agent logic
return '', 200
Webhook signature verification is non-negotiable in production. An unauthenticated inbound webhook is a direct vector for injecting fake emails into your agent's decision loop.
Pricing at scale
For an agent platform handling 50,000 inbound messages per month:
| Provider | Inbound cost | Notes |
|---|---|---|
| Mailgun Flex | ~$40 (blended) | Tied to sending plan, route complexity |
| SendGrid Essentials | $19.95/mo flat | Inbound included, but domain-level only |
| Postmark | $60.50 | Pro plan for 50K/mo; received mail counts toward volume |
| Mails.ai (Pro) | $20 | 50K inbound included, plus a separate 50K emails, no seat fee |
| Mails.ai + classify | $20 | Classification included on paid plans |
At this volume, Mails.ai Pro costs about a third of Postmark and about the same as SendGrid's flat fee — without the domain-level routing limitation. Between 3,000 and 10,000 inbound messages a month, Postmark's $16.50 Pro plan is the cheaper of the two. Classification adds nothing on Mails.ai's paid plans, while a single GPT-4o call for intent classification runs $0.003–0.01 per message depending on input length.
For a deeper look at how sender reputation and deliverability interacts with inbound — particularly for reply-heavy agent workflows — the architecture section covers the full picture.
Frequently Asked Questions
What's the difference between inbound email parsing and a full email API?
Inbound parsing is one component of an email API for agents. A full email API handles both directions: sending (SMTP injection, template rendering, bounce handling) and receiving (MX routing, MIME parsing, webhook delivery). Mails.ai provides both. Specialized tools like mx-email-parser handle only the MIME parsing step — you still need to provision MX records and handle SMTP reception separately.
How do threading fields like In-Reply-To work in practice?
In-Reply-To contains the Message-ID of the email being replied to. When your agent sends an email with a known Message-ID, any human reply will include In-Reply-To: <that-message-id>. That lets you correlate the reply to your agent's session without any application-level state lookup — the threading is self-contained in the headers. Mails.ai reads those headers for you and puts the conversation's thread_id on every reply event, so no header parsing is required.
Is inbound email parsing secure enough for sensitive workflows?
The parsing layer itself is passive — it receives and decodes. Security comes from three places: SPF/DKIM verification (confirm the sender domain is legitimate), webhook signature verification (confirm the webhook came from your provider, not an attacker), and your application's authorization logic (confirm the sender is allowed to trigger the action). Mails.ai verifies SPF/DKIM at ingestion and surfaces the results in each event's data, and signs all webhook deliveries with HMAC-SHA256.
Can I use inbound parsing without a custom domain?
Most inbound APIs do: an MX record on a domain you control is what routes mail to the provider's servers, so you can't use Gmail or a personal address as the inbound target. Mails.ai needs neither a domain nor an MX record. Every agent gets its own address, so hello sends from hello.yourcompany@send.mails.ai and its replies arrive at hello.yourcompany@in.mails.ai, each matched to its thread.
How does classification work and when should I enable it?
Mails.ai's classification reads first-contact mail (the message.received event) to an agent you turn it on for, on paid plans at no extra charge. Before the webhook fires, it adds a free-form intent label, the entities it found and an urgency score at the top level of the event. A reply to one of your own messages is never classified: its intent is general_reply, and it arrives matched to its thread instead. Enable it when your agent needs to branch on what a new sender wants — a demo, a refund, a question — and you want to skip the latency and cost of an inline LLM call per message. Skip it for pure logging, archival, or pass-through pipelines where you don't act on intent.
What's the fastest way to get started with Mails.ai inbound parsing?
Sign up at mails.ai: the workspace starts with an agent called hello, so there is no domain or DNS to set up. Set your webhook endpoint in the dashboard (or with POST /v1/webhooks), then send a message to reply@test.mails.ai, which answers within about a second, and your webhook receives a reply.received event. The @mailsai/sdk package (npm install @mailsai/sdk) includes typed webhook verification and event interfaces out of the box. The API reference covers address provisioning, webhook configuration, and event schemas.
Related guides: Best Email API for AI Agents 2026 | Full Comparison, SendGrid vs Mails.ai for Automated Email: 2026 Guide, Mails.ai vs Postmark for AI Agent Email: Full Comparison and AgentMail API vs Mails.ai: Best Email API for AI Agents.
Explore the product: Mails.ai vs SendGrid, Mails.ai vs Postmark, Mails.ai vs Mailgun, All features and Pricing.
