Inbound Email Parsing API: Receive Emails via Webhooks
How to receive emails in your app with an inbound email parsing API. Compare Postmark, SendGrid, Mailgun, and SendPigeon for webhook-based email parsing — with code examples.
Inbound email parsing lets your app receive emails as structured JSON via webhooks — no mail server required. Point your MX records to a parsing service, and every email sent to your domain hits your API as a POST request.
This is how support ticket systems, CRM tools, AI email agents, and reply-by-email features work under the hood.
How it works: MX record → parsing service → webhook POST to your app
Use cases: support tickets, reply-by-email, AI agents, CRM ingestion, document processing
Best options: Postmark (established), SendPigeon (all-in-one dev platform), SendGrid (enterprise), Mailgun (high volume)
How Inbound Email Parsing Works
User sends email → MX record routes to parsing service
↓
Service parses email
↓
POST to your webhook URL (JSON)
↓
Your app processes it
Three things happen:
- MX routing — Your domain's MX record points to the parsing service instead of (or alongside) a traditional mailbox.
- Parsing — The service extracts sender, recipient, subject, text body, HTML body, attachments, and headers into a structured payload.
- Webhook delivery — The parsed email is POSTed to your webhook URL as JSON. Your app handles it like any other API request.
No IMAP polling. No mail server maintenance. No POP3 connections.
Use Cases
Support tickets
Receive customer replies as webhooks. Match to existing tickets by threading headers or recipient address (e.g., ticket-123@support.yourdomain.com).
Reply-by-email
Let users reply to notification emails. Parse the reply body and add it as a comment, message, or update in your app.
AI email agents
Forward incoming emails to an LLM for classification, summarization, or automated response. The structured JSON format makes it trivial to extract what you need.
CRM and lead ingestion
Route sales inquiries to your CRM automatically. Parse sender info, extract intent from the body, create a lead record.
Document processing
Extract attachments (invoices, contracts, receipts) and process them through OCR, classification, or approval workflows.
Setting Up Inbound Parsing with SendPigeon
Step 1: Add your MX record
Point your domain (or subdomain) to SendPigeon's inbound server:
| Type | Priority | Value |
|---|---|---|
| MX | 10 | inbound.sendpigeon.dev |
Use a subdomain like inbound.yourdomain.com to keep inbound parsing separate from your regular email. This way your team's mailboxes stay unaffected.
Step 2: Configure your webhook
In the SendPigeon dashboard or via API, set your webhook URL:
curl -X PUT https://api.sendpigeon.dev/v1/domains/{domainId}/inbound \
-H "Authorization: Bearer sp_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"webhookEnabled": true,
"webhookUrl": "https://yourapp.com/api/inbound"
}'
SendPigeon generates a webhook secret automatically. Use it to verify incoming payloads.
Step 3: Handle the webhook
Every inbound email arrives as a POST request:
{
"event": "email.received",
"timestamp": "2026-04-13T10:30:00Z",
"data": {
"id": "in_abc123",
"from": "customer@example.com",
"to": "hello@yourdomain.com",
"subject": "Question about my order",
"text": "Hey, where's my package?",
"html": "<p>Hey, where's my package?</p>",
"rawUrl": "https://..."
}
}
Step 4: Verify the signature
Always verify webhooks before processing:
import { createHmac } from "crypto";
function verifyWebhook(
payload: string,
signature: string,
timestamp: string,
secret: string
): boolean {
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${payload}`)
.digest("hex");
return signature === expected;
}
// In your webhook handler
app.post("/api/inbound", (req, res) => {
const signature = req.headers["x-webhook-signature"] as string;
const timestamp = req.headers["x-webhook-timestamp"] as string;
if (!verifyWebhook(JSON.stringify(req.body), signature, timestamp, WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const { from, to, subject, text, html } = req.body.data;
// Process the email
console.log(`Email from ${from}: ${subject}`);
res.status(200).send("OK");
});
Never process unsigned webhooks in production. Without signature verification, anyone who discovers your webhook URL can inject fake emails into your system.
Replying to Inbound Emails
SendPigeon includes a reply endpoint that handles threading headers automatically:
curl -X POST https://api.sendpigeon.dev/v1/inbound/emails/{emailId}/reply \
-H "Authorization: Bearer sp_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"html": "<p>Your package ships tomorrow!</p>"
}'
The reply appears in the original email thread — correct In-Reply-To and References headers are set for you.
Comparing Inbound Email Parsing Services
| Feature | SendPigeon | Postmark | SendGrid | Mailgun |
|---|---|---|---|---|
| Webhook delivery | Yes | Yes | Yes | Yes |
| Signature verification | HMAC-SHA256 | Basic Auth | OAuth | HMAC-SHA256 |
| Reply with threading | Dedicated endpoint | Manual headers | Manual headers | Manual headers |
| Attachment access | Raw email URL | Base64 in payload | Base64 in payload | URL or Base64 |
| Also sends email | Yes (API + SMTP) | Yes | Yes | Yes |
| Broadcasts + sequences | Yes | No | Marketing via separate product | No |
| Rate limit | 100/min per domain | 10K/hr | Varies by plan | Varies by plan |
| Pricing | Included in paid plan | $15/mo+ | Free tier available | $0.80/1K emails |
Postmark
The most established option for inbound parsing. Clean API, reliable delivery, thorough documentation. Focused on transactional email and inbound — broadcasts and sequences require separate tools.
SendGrid
Part of the Twilio ecosystem. Inbound Parse requires additional configuration (parse settings, hostname verification, MX + CNAME records). Strong choice if you're already using Twilio services.
Mailgun
High-volume friendly with flexible routing rules. Supports pattern matching on recipient addresses for advanced inbound routing. Requires more initial configuration than some alternatives.
SendPigeon
Transactional, broadcasts, sequences, and inbound in one platform. Includes a reply endpoint with automatic threading. One tool for sending and receiving email.
Building an Inbound Email Handler (Full Example)
Here's a complete Express handler that receives inbound emails, verifies the signature, and creates a support ticket:
import express from "express";
import { createHmac } from "crypto";
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.SENDPIGEON_WEBHOOK_SECRET!;
app.post("/api/inbound", async (req, res) => {
// 1. Verify signature
const signature = req.headers["x-webhook-signature"] as string;
const timestamp = req.headers["x-webhook-timestamp"] as string;
const payload = JSON.stringify(req.body);
const expected = createHmac("sha256", WEBHOOK_SECRET)
.update(`${timestamp}.${payload}`)
.digest("hex");
if (signature !== expected) {
return res.status(401).send("Invalid signature");
}
// 2. Extract email data
const { id, from, to, subject, text, html } = req.body.data;
// 3. Route by recipient address
if (to.startsWith("support@")) {
await createSupportTicket({ from, subject, body: text || html, inboundId: id });
} else if (to.startsWith("billing@")) {
await routeToBilling({ from, subject, body: text || html });
} else {
console.log(`Unhandled inbound email to ${to}`);
}
// 4. Always respond 200 to acknowledge receipt
res.status(200).send("OK");
});
app.listen(3000);
Always return 200 quickly. Do heavy processing asynchronously (queue the work) so the webhook doesn't time out. SendPigeon retries failed deliveries 3 times with exponential backoff.
Common Patterns
Route by recipient address
Use address-based routing to map emails to different parts of your app:
support@yourdomain.com→ Support ticket systembilling@yourdomain.com→ Billing pipelineticket-{id}@yourdomain.com→ Specific ticket threadreply+{token}@yourdomain.com→ Authenticated reply matching
Extract structured data
Parse the email body for specific information:
// Extract order number from subject
const orderMatch = subject.match(/order #(\d+)/i);
if (orderMatch) {
const orderId = orderMatch[1];
await linkEmailToOrder(orderId, inboundId);
}
Forward to AI
Send the parsed email to an LLM for classification or response:
const classification = await llm.classify({
prompt: `Classify this support email:
From: ${from}
Subject: ${subject}
Body: ${text}
Categories: billing, technical, feature-request, spam`,
});
await routeToTeam(classification.category, inboundId);
MX Record Tips
- Use a subdomain (
inbound.yourdomain.com) to avoid conflicts with your team's email - MX record changes can take up to 48 hours to propagate, but usually take 15-30 minutes
- You can have multiple MX records with different priorities for fallback routing
- Test your setup with SendPigeon's built-in webhook tester before going live
Pricing
Inbound email parsing is included in all SendPigeon paid plans — no extra charge. Starter ($10/mo) includes 1,000 inbound emails/month, Growth ($15/mo) includes 5,000, and Pro ($39/mo) includes 25,000. For a full pricing comparison across providers, see Postmark alternatives.
Next Steps
- Set up inbound email in the SendPigeon docs
- Learn about webhook security and signature verification
- Build email sequences to automate replies to inbound emails
- Compare email testing tools in 7 Best MailHog Alternatives
- Check your domain health with our email authentication checker