Skip to content
Dashboard

How to Receive Emails in Next.js with Webhooks

Content Engineer

Copy link to headingWhat you need before you start

Copy link to headingSet up a receiving address

Copy link to headingUsing the .resend.app address

Copy link to headingUsing your own domain with an MX record

Copy link to headingHandle the email.received webhook in a route handler

{
"type": "email.received",
"created_at": "2026-02-22T23:41:12.126Z",
"data": {
"email_id": "56761188-7520-42d8-8898-ff6fc54ce618",
"created_at": "2026-02-22T23:41:11.894719+00:00",
"from": "onboarding@resend.dev",
"to": ["delivered@resend.dev"],
"bcc": [],
"cc": [],
"received_for": ["forwarded@example.com"],
"message_id": "<111-222-333@email.example.com>",
"subject": "Sending this example",
"attachments": [
{
"id": "2a0c9ce0-3112-4728-976e-47ddcd16a318",
"filename": "avatar.png",
"content_type": "image/png",
"content_disposition": "inline",
"content_id": "img001"
}
]
}
}

app/api/inbound/route.ts
import { NextResponse } from 'next/server';
type ReceivedAttachment = {
id: string;
filename: string;
content_type: string;
content_disposition: string | null;
content_id: string | null;
};
type EmailReceivedEvent = {
type: 'email.received';
created_at: string;
data: {
email_id: string;
created_at: string;
from: string;
to: string[];
cc: string[];
bcc: string[];
received_for: string[];
message_id: string;
subject: string;
attachments: ReceivedAttachment[];
};
};
export async function POST(request: Request) {
const event = (await request.json()) as EmailReceivedEvent;
if (event.type !== 'email.received') {
return NextResponse.json({ ok: true });
}
const { email_id, from, to, subject, attachments } = event.data;
// Every address at your domain arrives here. Route on `to`.
if (to.some((address) => address.startsWith('support@'))) {
console.log(`Support email ${email_id} from ${from}: ${subject}`);
console.log(`${attachments.length} attachment(s)`);
}
return NextResponse.json({ ok: true });
}

Copy link to headingWhy the webhook doesn't include the email body

const { data: email } = await resend.emails.receiving.get(event.data.email_id);

Copy link to headingGet the email content

app/api/inbound/route.ts
import { NextResponse } from 'next/server';
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function POST(request: Request) {
const event = (await request.json()) as EmailReceivedEvent;
if (event.type !== 'email.received') {
return NextResponse.json({ ok: true });
}
const { data: email, error } = await resend.emails.receiving.get(
event.data.email_id,
);
if (error || !email) {
console.error('Could not fetch email', error);
return NextResponse.json({ ok: false }, { status: 500 });
}
console.log(email.subject);
console.log(email.text); // null when the message has no plain text part
console.log(email.html);
console.log(email.headers);
return NextResponse.json({ ok: true });
}

Copy link to headingDownload attachments

// Inside your POST handler, after checking event.type.
const { data: list, error } = await resend.emails.receiving.attachments.list({
emailId: event.data.email_id,
});
if (error || !list) {
console.error('Could not list attachments', error);
return NextResponse.json({ ok: false }, { status: 500 });
}
for (const attachment of list.data) {
console.log(attachment.filename, attachment.content_type, attachment.size);
const response = await fetch(attachment.download_url);
const bytes = Buffer.from(await response.arrayBuffer());
// Upload `bytes` to your own storage here.
}

Copy link to headingReply in the same thread

// Inside your POST handler, after checking event.type.
const { data, error } = await resend.emails.send({
from: 'Support <support@yourdomain.tld>',
to: [event.data.from],
subject: `Re: ${event.data.subject}`,
html: '<p>Thanks, we got your message.</p>',
headers: {
'In-Reply-To': event.data.message_id,
},
});

headers: {
'In-Reply-To': event.data.message_id,
References: [...previousMessageIds, event.data.message_id].join(' '),
},

Copy link to headingVerify the webhook signature

app/api/inbound/route.ts
import { NextResponse } from 'next/server';
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function POST(request: Request) {
const payload = await request.text();
let event: EmailReceivedEvent;
try {
event = resend.webhooks.verify({
payload,
headers: {
id: request.headers.get('svix-id') ?? '',
timestamp: request.headers.get('svix-timestamp') ?? '',
signature: request.headers.get('svix-signature') ?? '',
},
webhookSecret: process.env.RESEND_WEBHOOK_SECRET ?? '',
}) as EmailReceivedEvent;
} catch {
return new NextResponse('Invalid webhook', { status: 400 });
}
if (event.type !== 'email.received') {
return NextResponse.json({ ok: true });
}
// Handle the verified event.
return NextResponse.json({ ok: true });
}

Copy link to headingTest it locally

resend webhooks listen \
--url <https://your-public-url> \
--events email.received \
--forward-to <http://localhost:3000/api/inbound>

resend emails receiving listen

Copy link to headingFAQ

Copy link to headingWhat to build next

Ready to deploy?