← Blog

Blog

Inbound Email Notification Webhooks (Next.js, Vercel)

Sahand Seifi 10 min read
  • Engineering
  • Guides
  • Email
  • Webhooks
  • Next.js

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

AspectPingramDIY (SES + S3 + Next.js)
Setup TimeMinutesDays
MX RecordsOptionalRequired
Email ParsingManagedYou build it
AttachmentsIncluded in payloadManual extraction
Reply ThreadingAutomaticManual implementation
CostGenerous free tierSNS, S3, SES costs
Other ChannelsEmail + SMS + VoiceEmail only

Option 1: Pingram (Ship in Minutes)

  1. Create a Pingram account and add your domain
  2. Go to Settings → Webhooks, enable the EMAIL_INBOUND event, and set the URL to https://your-app.vercel.app/api/inbound-email
  3. Create app/api/inbound-email/route.ts to 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 });
}
  1. 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

Terminal window
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_INBOUND webhooks, 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.

Ready when you are

Launch today.

Free tier with 3,000 emails, plus 100 SMS and calls per month. No credit card required. Paid plans start at $20/month.