BLOGS
Inbound Email Notification Webhooks (Next.js, Vercel)
Build a serverless email processing pipeline in Next.js on Vercel. Receive inbound emails as webhooks, parse content and attachments, and handle Vercel-specific limits.
Motivation
You’re building a SaaS app on Next.js and Vercel. A customer replies to a notification email, a support ticket comes in, or an AI agent needs to read incoming messages. You need that email turned into structured data your app can act on.
The traditional route of spinning up an SMTP server, configuring MX records, parsing raw MIME doesn’t fit the modern development workflow and serverless architecture.
An elegant solution is an Email to JSON/Webhook service: emails arrive, get parsed, and hit your Next.js route handler as a clean JSON webhook.
This guide covers two approaches: using Pingram’s managed inbound webhooks (minutes to set up) and building your own pipeline (full control, more work). Both deploy to Vercel with zero infrastructure to manage.
Quick Comparison
| Aspect | Pingram | DIY (SES + S3 + Next.js) |
|---|---|---|
| Setup Time | Minutes | Days |
| MX Records | Optional | Required |
| Email Parsing | Managed | You build it |
| Attachments | Included in payload | Manual extraction |
| Reply Threading | Automatic | Manual implementation |
| Cost | Generous free tier | SNS, S3, SES costs |
| Other Channels | Email + SMS + Voice | Email only |
Option 1: Pingram (Ship in Minutes)
- Create a Pingram account and add your domain
- Go to Settings → Webhooks, enable the
EMAIL_INBOUNDevent, and set the URL tohttps://your-app.vercel.app/api/inbound-email - Create
app/api/inbound-email/route.tsto receive the webhook:
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const payload = await request.json();
if (payload.eventType === 'EMAIL_INBOUND') {
console.log(payload.from);
console.log(payload.subject);
console.log(payload.bodyText);
console.log(payload.attachments);
}
return NextResponse.json({ received: true });
}
- Pingram gives you a test inbox that you can send emails to, to test your webhook immediately. Optionally, set up your own inbound address from Dashboard -> Inbound.
As you can see, using Pingram you can receive inbound emails in under 30 lines of code.
Option 2: Build Your Own Pipeline
If you need full control over the email reception layer, you can wire AWS SES to your Next.js API route.
Architecture
Incoming Email → MX Records → AWS SES → S3 Bucket → SNS Topic → Next.js API Route
1. Configure AWS SES for Inbound
Verify your domain in SES and point your MX records to the SES inbound endpoint:
MX 10 inbound-smtp.us-east-1.amazonaws.com
Create an SES receipt rule that stores incoming emails in an S3 bucket and publishes a notification to an SNS topic.
2. Create an SNS-to-Webhook Bridge
Set up an SNS HTTP/S subscription pointing to your Vercel endpoint. SNS will POST a notification whenever a new email lands in S3.
3. Parse the Email in Your Route Handler
import { NextResponse } from 'next/server';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { simpleParser, type ParsedMail } from 'mailparser';
const s3 = new S3Client({ region: 'us-east-1' });
export async function POST(request: Request) {
const snsMessage = await request.json();
// SNS subscription confirmation
if (snsMessage.Type === 'SubscriptionConfirmation') {
await fetch(snsMessage.SubscribeURL);
return NextResponse.json({ confirmed: true });
}
const notification = JSON.parse(snsMessage.Message);
const bucket = notification.receipt.action.bucketName;
const key = notification.receipt.action.objectKey;
// Fetch raw email from S3
const { Body } = await s3.send(
new GetObjectCommand({ Bucket: bucket, Key: key })
);
const rawEmail = await Body!.transformToString();
// Parse MIME content
const parsed: ParsedMail = await simpleParser(rawEmail);
console.log(`From: ${parsed.from?.text}`);
console.log(`Subject: ${parsed.subject}`);
console.log(`Body: ${parsed.text}`);
console.log(`Attachments: ${parsed.attachments.length}`);
// Process attachments
for (const attachment of parsed.attachments) {
// attachment.content is a Buffer
// attachment.filename, attachment.contentType, attachment.size
}
return NextResponse.json({ processed: true });
}
4. Install Dependencies
npm install @aws-sdk/client-s3 mailparser
npm install -D @types/mailparser
Trade-offs of DIY:
- Full control over every step of the pipeline
- No vendor dependency for email reception
- But: you own MX records, SES configuration, S3 lifecycle policies, SNS subscriptions, MIME parsing edge cases, retry logic, and conversation threading
Deployment Considerations & Gotchas
There are platform-specific constraints on Vercel to plan for:
Request Body Size: 4.5 MB Limit
Vercel serverless functions cap request bodies at 4.5 MB. Most text-only emails are well under this, but emails with large attachments can exceed it. Strategies:
- With Pingram: Configure attachment size limits in the dashboard, or use Pingram’s attachment URL feature to receive download links instead of inline base64
Function Timeout: Up to 300s (Hobby) / 800s (Pro)
Email processing should respond quickly: return 200 immediately, then process asynchronously if needed. For heavy processing (AI extraction, database writes, external API calls), consider the waitUntil vercel functionality that lets the function continue running after the response is sent, avoiding webhook timeout retries.
Idempotency
Email providers may retry webhook delivery. Use trackingId (Pingram) or messageId (DIY) to deduplicate.
Common Challenges
Email Threading
When a user replies to an email you sent, you need to match the reply to the original conversation. Pingram includes a trackingId that links to the original notification, making this trivial. With DIY, you’d parse In-Reply-To and References headers from the raw MIME content.
Character Encoding
Emails arrive in every encoding imaginable—UTF-8, ISO-8859-1, Windows-1252, base64, quoted-printable. Pingram normalizes everything to UTF-8 in the webhook payload. With DIY, mailparser handles most cases, but edge cases exist with non-standard Asian encodings and legacy clients.
Bounce and Spam Handling
Not every inbound email is legitimate. Expect bounce notifications, auto-replies, spam, and out-of-office messages hitting your webhook. Filter these early:
const autoReplyPatterns = [
/^(auto|out.of.office|automatic.reply)/i,
/^(noreply|no-reply|mailer-daemon)/i
];
function isAutoReply(payload: InboundEmailPayload): boolean {
return autoReplyPatterns.some(
(pattern) => pattern.test(payload.subject) || pattern.test(payload.from)
);
}
TLDR
-
Pingram: Fastest path. Add your domain, enable
EMAIL_INBOUNDwebhooks, write a route handler. Threading, parsing, and attachments are handled for you in a generous free tier. -
DIY (SES + S3): Full control. Wire AWS SES → S3 → SNS → your Next.js route. You own MIME parsing, threading, and retry logic; and pay for SES, S3, SNS resources.
Both approaches work on Vercel. Watch the 4.5 MB body limit for attachments and use waitUntil for heavy processing.