How to Receive Emails in Next.js (Inbound Parsing)
Receive incoming emails in your Next.js app using webhooks and inbound email parsing. Complete guide with App Router, signature verification, and TypeScript examples.
To receive emails in Next.js, set up an inbound parsing service that converts incoming emails to webhook POST requests, then handle them in an API route. No mail server needed.
How it works:
- Add an MX record pointing to
inbound.sendpigeon.dev - Create an API route to handle webhook POSTs
- Verify the signature, process the email
Works on: Vercel, AWS, self-hosted — any Next.js deployment.
Prerequisites
- A domain you control (for MX records)
- A SendPigeon account with a paid plan
- A deployed Next.js app (or a tunnel like ngrok for local dev)
Step 1: Add the MX Record
Point your domain (or a subdomain) to SendPigeon's inbound server:
| Type | Priority | Value |
|---|---|---|
| MX | 10 | inbound.sendpigeon.dev |
Use a subdomain like inbound.yourdomain.com so your team's regular email stays unaffected.
Step 2: Configure the Webhook
In the SendPigeon dashboard, go to your domain's inbound settings and set the webhook URL to your Next.js API route:
https://yourapp.com/api/inbound
Save the webhook secret — you'll need it for verification.
Step 3: Create the Webhook Handler
App Router (Route Handler)
// app/api/inbound/route.ts
import { createHmac } from "crypto";
import { NextResponse } from "next/server";
const WEBHOOK_SECRET = process.env.SENDPIGEON_WEBHOOK_SECRET!;
function verifySignature(payload: string, signature: string, timestamp: string): boolean {
const expected = createHmac("sha256", WEBHOOK_SECRET)
.update(`${timestamp}.${payload}`)
.digest("hex");
return expected === signature;
}
export async function POST(request: Request) {
const signature = request.headers.get("x-webhook-signature");
const timestamp = request.headers.get("x-webhook-timestamp");
const body = await request.text();
if (!signature || !timestamp || !verifySignature(body, signature, timestamp)) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
const { event, data } = JSON.parse(body);
if (event === "email.received") {
const { id, from, to, subject, text, html } = data;
// Your logic here — create ticket, notify team, trigger workflow
console.log(`Inbound email from ${from}: ${subject}`);
// Example: save to database
// await db.inboundEmail.create({ data: { from, to, subject, body: text } });
}
return NextResponse.json({ received: true });
}
Always verify the webhook signature before processing. Without it, anyone who discovers your webhook URL can inject fake emails.
Step 4: Test Locally with ngrok
During development, use ngrok to expose your local Next.js server:
ngrok http 3000
Set the ngrok URL as your webhook in the SendPigeon dashboard:
https://abc123.ngrok-free.app/api/inbound
Or use SendPigeon's built-in webhook tester in the dashboard to send a sample payload.
Handling Different Email Types
Route emails based on the recipient address:
export async function POST(request: Request) {
// ... signature verification ...
const { data } = JSON.parse(body);
const { to, from, subject, text, html, id } = data;
if (to.startsWith("support@")) {
await createSupportTicket({ from, subject, body: text || html });
} else if (to.startsWith("billing@")) {
await forwardToBilling({ from, subject, body: text || html });
} else if (to.match(/^reply\+(.+)@/)) {
const token = to.match(/^reply\+(.+)@/)![1];
await addReplyToThread(token, { from, body: text || html });
}
return NextResponse.json({ received: true });
}
Replying to Inbound Emails
Use the SendPigeon API to reply with proper threading:
async function replyToEmail(inboundId: string, html: string) {
const res = await fetch(
`https://api.sendpigeon.dev/v1/inbound/emails/${inboundId}/reply`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SENDPIGEON_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ html }),
}
);
return res.json();
}
The reply lands in the sender's inbox in the original email thread — In-Reply-To and References headers are handled for you.
Processing Attachments
The webhook payload includes a rawUrl — a presigned URL to the original .eml file. Use it to access attachments:
async function getAttachments(rawUrl: string) {
const res = await fetch(rawUrl);
const rawEmail = await res.text();
// Parse with a library like mailparser
// const { attachments } = await simpleParser(rawEmail);
return rawEmail;
}
Production Checklist
- Webhook URL is HTTPS (required)
- Signature verification is in place
- Handler returns 200 quickly (queue heavy work)
- MX record is set on your domain/subdomain
- Error handling doesn't leak internal details
- Test webhook works from SendPigeon dashboard
SendPigeon retries failed webhook deliveries 3 times with exponential backoff. Always return 200 to acknowledge receipt, even if you process asynchronously.
Next Steps
- Read the full inbound email parsing guide
- Set up inbound email in the docs
- Learn how to send email in Next.js with the same SDK
- Build an AI email agent that auto-responds to inbound emails