Why inbound email processing matters for product teams and platforms
Inbound email processing is the backbone for features like ticketing pipelines, bidirectional notifications, threaded discussions, automated approvals, and user-generated content intake. When your application can receive, route, and process emails programmatically, you unlock reliable two-way workflows without forcing users to learn new interfaces. The difference between a smooth developer experience and a fragile integration often comes down to how the provider handles MIME parsing, attachments, authentication, retries, and delivery options.
This comparison focuses specifically on inbound-email-processing capabilities - receiving,, routing,, processing - not outbound delivery or templates. If you are exploring architecture patterns and potential product ideas, you may also find these resources helpful:
- Top Inbound Email Processing Ideas for SaaS Platforms
- Email Infrastructure Checklist for SaaS Platforms
The right solution should offer predictable JSON, straightforward verification, flexible delivery, and strong safeguards for edge cases. With that lens, let's evaluate how MailParse and Postmark Inbound approach the problem.
How MailParse handles inbound email processing
MailParse provides instant addresses, accepts messages, parses MIME into normalized JSON, and delivers message events via webhook or exposes them via a REST polling API. That dual model gives teams immediate push delivery while preserving a reliable pull-based fallback for maintenance windows or network isolation.
Inbound addresses and routing
- Provision addresses instantly per user, per organization, or per feature. Wildcards and subaddressing enable flexible routing keys.
- Configurable routing rules match on envelope recipients, headers, subject patterns, or attachment presence, then tag and forward to the correct webhook or queue.
Webhook delivery and verification
- Events are signed using HMAC-SHA256 with your webhook secret. The platform includes a timestamp and nonce in headers to prevent replay.
- Automatic retries with exponential backoff and jitter. You can return 2xx to acknowledge or 4xx to drop. 5xx will trigger retries up to a configurable limit.
REST polling API
- List messages by status, recipient, tags, or time range. This is ideal when your app cannot accept inbound HTTP from the public internet.
- Idempotent acknowledgment endpoints let you mark messages as processed, requeue them, or dead-letter on repeated failures.
Structured JSON schema for inbound messages
The normalized JSON includes fields designed for application logic and storage:
- Message identifiers: id, message_id, in_reply_to, references.
- Addresses: from, to[], cc[], bcc[], plus parsed name and email for each.
- Content: subject, text, html, a reply_stripped field that contains just the new reply content when available.
- Attachments: array with filename, content_type, size, content_id, checksum, and a secure download URL or byte content for small files.
- Authentication signals: SPF, DKIM, DMARC results where available.
- Raw headers for advanced matching and auditing.
For many teams, the key differentiator is the availability of both webhook and polling. That flexibility reduces operational risk when you are deploying, rotating infrastructure, or segmenting protected networks.
How Postmark Inbound handles inbound email processing
Postmark Inbound delivers incoming messages exclusively via a webhook you configure per server. It parses the message and sends a JSON payload that includes FromFull, ToFull, CcFull, Subject, Date, HtmlBody, TextBody, StrippedTextReply, Headers, MessageID, and an Attachments array where file content is base64 encoded. The service adds an HMAC signature header (X-Postmark-Signature) that you can verify using your webhook token.
Operationally, the model is simple and effective for push-centric architectures. You get immediate delivery to your handler, built-in retry behavior for non-2xx responses, and a consistent schema that has been stable for years. The main tradeoff is that there is no REST polling option to pull pending inbound messages, so your handler must be reachable from the public internet or you will need a secure relay or queue to bridge networks.
Side-by-side comparison for inbound email processing
| Capability | MailParse | Postmark Inbound |
|---|---|---|
| Delivery options | Webhook and REST polling | Webhook only |
| Instant address provisioning | Yes - per user, per org, with wildcards | Addresses via your domain with routing to server |
| JSON fields for threading | message_id, in_reply_to, references | MessageID, References in headers |
| Reply stripping | reply_stripped plus full text and html | StrippedTextReply plus TextBody and HtmlBody |
| Attachments | Metadata plus download URL or inline content | Array with base64 content |
| Authentication signals | SPF, DKIM, DMARC fields exposed | Available via headers and parsed fields |
| Webhook signing | HMAC-SHA256 with timestamp and nonce | HMAC-SHA256 via X-Postmark-Signature |
| Webhook retries | Configurable backoff with jitter | Automatic retries for non-2xx |
| Message lifecycle APIs | List, ack, requeue, dead-letter | Not applicable - webhook only |
| Data residency options | Regional processing options available | Depends on Postmark regions |
| Sandbox and test utilities | Test addresses and sample payloads | Server tokens and testing via webhook endpoints |
Code examples
Webhook receiver with HMAC verification (generic platform)
This example shows a Node.js Express handler that verifies a signed webhook and processes attachments. It mirrors the approach used by the platform without tying to a specific domain.
import crypto from "crypto";
import express from "express";
const app = express();
// Capture raw body for signature verification
app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
function verifySignature(req) {
const signature = req.header("X-Signature");
const timestamp = req.header("X-Timestamp");
const body = req.rawBody || Buffer.from("");
const payload = Buffer.concat([Buffer.from(timestamp || ""), body]);
const expected = crypto.createHmac("sha256", WEBHOOK_SECRET).update(payload).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature || "", "utf8"));
}
app.post("/inbound", (req, res) => {
if (!verifySignature(req)) {
return res.status(401).send("invalid signature");
}
const event = req.body; // normalized JSON
const { id, subject, from, text, html, attachments = [] } = event;
// Save message to database
// db.messages.insert({ id, subject, from, text, html });
// Download attachments by URL if provided
attachments.forEach(a => {
if (a.download_url) {
// fetch(a.download_url, { headers: { Authorization: "Bearer <token>" } })
// .then(r => r.arrayBuffer())
// .then(buf => saveFile(a.filename, Buffer.from(buf)));
}
});
// Acknowledge so delivery system can stop retrying
return res.status(204).end();
});
app.listen(3000, () => console.log("listening on :3000"));
Polling recent inbound messages via REST
import fetch from "node-fetch";
const API_KEY = process.env.API_KEY;
async function poll() {
const r = await fetch("https://api.example.com/v1/messages?status=pending&limit=50", {
headers: { Authorization: `Bearer ${API_KEY}` }
});
const { data } = await r.json();
for (const msg of data) {
// Process message...
// await processMessage(msg);
// Mark as acknowledged
await fetch(`https://api.example.com/v1/messages/${msg.id}/ack`, {
method: "POST",
headers: { Authorization: `Bearer ${API_KEY}` }
});
}
}
setInterval(poll, 2000);
Postmark Inbound webhook receiver with signature verification
Postmark Inbound expects your server to handle a JSON POST and verify the X-Postmark-Signature header using your webhook token. Here is a minimal Express server that validates the HMAC and parses the payload.
import crypto from "crypto";
import express from "express";
const app = express();
app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));
const POSTMARK_WEBHOOK_TOKEN = process.env.POSTMARK_WEBHOOK_TOKEN;
function verifyPostmarkSignature(req) {
const signature = req.header("X-Postmark-Signature");
const computed = crypto
.createHmac("sha256", POSTMARK_WEBHOOK_TOKEN)
.update(req.rawBody || Buffer.from(""))
.digest("base64");
return signature === computed;
}
app.post("/postmark/inbound", (req, res) => {
if (!verifyPostmarkSignature(req)) {
return res.status(401).send("invalid signature");
}
const inbound = req.body;
const subject = inbound.Subject;
const from = inbound.FromFull?.Email;
const text = inbound.TextBody;
const replyOnly = inbound.StrippedTextReply;
// Decode attachments if present
for (const att of inbound.Attachments || []) {
const buf = Buffer.from(att.Content, "base64");
// saveFile(att.Name, buf, att.ContentType);
}
// Persist message or route to your workers
// db.messages.insert({ subject, from, text, replyOnly });
res.status(200).send("ok");
});
app.listen(3000, () => console.log("postmark inbound listening on :3000"));
Python Flask example for inbound processing
from flask import Flask, request, abort
import hmac, hashlib, base64, json
app = Flask(__name__)
WEBHOOK_SECRET = b"your-secret"
@app.route("/inbound", methods=["POST"])
def inbound():
raw = request.get_data()
sig = request.headers.get("X-Signature", "")
expected = hmac.new(WEBHOOK_SECRET, request.headers.get("X-Timestamp", "").encode() + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(401)
event = request.json
# process event...
return ("", 204)
Performance and reliability considerations
Retries and backpressure
Push delivery is excellent for low latency, but you need a robust retry model and a way to avoid amplifying failures. Webhooks should include exponential backoff, jitter, and a sane cutoff. If your downstream becomes slow, having a queue or pull-based option prevents dropped messages. The presence of a REST polling API is useful as a fallback during maintenance or deploys, and it simplifies processing from restricted networks.
Attachment handling
Inline base64 attachments simplify single-request delivery but can increase payload size and memory pressure on your handler. Download URLs reduce webhook size and let your worker fetch attachments on demand with controlled concurrency. Both patterns work. Choose based on your traffic profile and resource limits. Either way, verify content_type, file size, and checksums before writing to disk.
Threading and reply extraction
For ticketing and discussions, you should rely on in_reply_to and references where available, then fall back to subject tokens or mailbox hashes. Reply stripping avoids duplicating quoted history. Test with popular clients like Gmail, Outlook, Apple Mail to ensure consistent parsing of signatures and footers.
Authentication and security
- Verify webhook signatures, then enforce timestamp windows to mitigate replay.
- Validate SPF, DKIM, DMARC results to score or reject messages that fail policy.
- Normalize and whitelist content types, and scan attachments with your chosen malware scanner.
- Use idempotency keys derived from transport and Message-ID to avoid duplicates when retries occur.
Monitoring and observability
Expose metrics for ingress rate, parse errors, webhook response times, retry counts, and backlog. Keep a dead-letter queue for irrecoverable messages and replay tooling for safe reprocessing. If you run support workflows, pair this with an operational checklist like the Email Infrastructure Checklist for Customer Support Teams.
Verdict: which is better for inbound email processing?
Postmark Inbound is a solid choice when you want a straightforward webhook-first approach and you already run application servers that are reachable from the public internet. Its JSON schema is predictable, the signature mechanism is clear, and it has long-term production track record. The primary limitation is the lack of a REST polling option. If your network architecture or change windows make push-only delivery risky, you will need to build your own buffering layer.
MailParse is stronger for teams that want both immediate push delivery and a reliable pull-based fallback, or who prefer to process inbound messages from private networks without a public ingress. The normalized JSON focuses on application primitives like reply_stripped, explicit threading fields, and attachment metadata with secure downloads. If flexibility and operational resilience are priorities, the platform offers more control.
In short: choose Postmark Inbound if webhook-only suits your architecture. Choose MailParse if you need webhook plus REST polling and deeper control over message lifecycle.
FAQ
Can I consume inbound emails without exposing a public webhook?
Yes. With a polling API you can keep your processing workers in private subnets and pull messages on a schedule. Without polling, you would need a secure reverse proxy or queue to buffer webhook traffic at the edge.
How should I handle large attachments and timeouts?
Use streaming downloads or chunked processing where possible, set a maximum accepted size, and move heavy work to background jobs. For webhook timeouts, quickly enqueue and return 2xx, then process asynchronously to avoid retry storms.
What is the best way to thread replies into conversations?
Prefer in_reply_to and references when present. Fall back to stable tokens like a mailbox hash or a synthetic thread key embedded in the local part of the recipient. Keep subject-only heuristics as a last resort.
How do I prevent duplicate processing with retries?
Compute a deterministic idempotency key from the provider delivery id plus the email's Message-ID. Store it in a fast key-value store. If you see the same key again, acknowledge and skip processing.
What checklists should I use before launching inbound features?
Review authentication, retries, storage, and data protection. These two guides are practical starting points: Email Deliverability Checklist for SaaS Platforms and Email Infrastructure Checklist for SaaS Platforms.