Back to blog
Inbound EmailEmail ParsingWebhooksDeveloper GuideAPI

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.

SendPigeon TeamApril 7, 20267 min read

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.

TL;DR

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:

  1. MX routing — Your domain's MX record points to the parsing service instead of (or alongside) a traditional mailbox.
  2. Parsing — The service extracts sender, recipient, subject, text body, HTML body, attachments, and headers into a structured payload.
  3. 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:

TypePriorityValue
MX10inbound.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

FeatureSendPigeonPostmarkSendGridMailgun
Webhook deliveryYesYesYesYes
Signature verificationHMAC-SHA256Basic AuthOAuthHMAC-SHA256
Reply with threadingDedicated endpointManual headersManual headersManual headers
Attachment accessRaw email URLBase64 in payloadBase64 in payloadURL or Base64
Also sends emailYes (API + SMTP)YesYesYes
Broadcasts + sequencesYesNoMarketing via separate productNo
Rate limit100/min per domain10K/hrVaries by planVaries by plan
PricingIncluded 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 system
  • billing@yourdomain.com → Billing pipeline
  • ticket-{id}@yourdomain.com → Specific ticket thread
  • reply+{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