Resend Integration
Complete guide for integrating Resend email services into Next.js applications with proper Audiences setup.
When to Use
-
Setting up newsletter signups
-
Adding contact form email notifications
-
Implementing booking/calendar email confirmations
-
Configuring email forwarding via webhooks
-
Managing multi-domain Resend accounts
Resend Audiences Architecture
Resend has ONE audience per account. Use these features to organize:
Feature Purpose Visibility
Contacts Individual subscribers
Properties Custom data fields (domain, source, company) Internal
Segments Internal groupings for targeting Internal
Topics User-facing email preferences User can manage
Broadcasts Campaign sending with auto-unsubscribe
Multi-Domain Strategy
For accounts with multiple domains, tag contacts with properties:
await resend.contacts.create({ email, properties: { domain: "example.com", // Which project source: "newsletter", // How they signed up }, segments: [{ id: SEGMENT_ID }], topics: [{ id: TOPIC_ID, subscription: "opt_in" }], });
Implementation
- Shared Utility (lib/resend.ts )
import { Resend } from "resend";
export const resend = new Resend(process.env.RESEND_API_KEY);
const SEGMENT_NEWSLETTER = process.env.RESEND_SEGMENT_NEWSLETTER; const SEGMENT_LEADS = process.env.RESEND_SEGMENT_LEADS; const TOPIC_NEWSLETTER = process.env.RESEND_TOPIC_NEWSLETTER;
type ContactSource = "newsletter" | "booking" | "contact";
interface CreateContactOptions { email: string; firstName?: string; lastName?: string; company?: string; source: ContactSource; subscribeToNewsletter?: boolean; }
export async function createContact({ email, firstName, lastName, company, source, subscribeToNewsletter = false, }: CreateContactOptions) { const segments: { id: string }[] = []; if (source === "newsletter" && SEGMENT_NEWSLETTER) { segments.push({ id: SEGMENT_NEWSLETTER }); } else if ((source === "booking" || source === "contact") && SEGMENT_LEADS) { segments.push({ id: SEGMENT_LEADS }); }
const topics: { id: string; subscription: "opt_in" | "opt_out" }[] = []; if (subscribeToNewsletter && TOPIC_NEWSLETTER) { topics.push({ id: TOPIC_NEWSLETTER, subscription: "opt_in" }); }
const properties: Record<string, string> = { domain: "YOUR_DOMAIN.com", // Replace with actual domain source, }; if (company) properties.company = company;
const { data, error } = await resend.contacts.create({ email, firstName: firstName || undefined, lastName: lastName || undefined, unsubscribed: false, ...(Object.keys(properties).length > 0 && { properties }), ...(segments.length > 0 && { segments }), ...(topics.length > 0 && { topics }), });
if (error?.message?.includes("already exists")) { return { exists: true, error: null }; } return { data, exists: false, error }; }
export async function contactExists(email: string): Promise<boolean> { try { const { data } = await resend.contacts.get({ email }); return !!data; } catch { return false; } }
- Newsletter Route (/api/newsletter )
import { NextResponse } from "next/server"; import { resend, createContact, contactExists } from "@/lib/resend";
export async function POST(request: Request) { const { email } = await request.json();
if (!email) { return NextResponse.json({ error: "Email is required" }, { status: 400 }); }
// Duplicate check if (await contactExists(email)) { return NextResponse.json( { error: "already_subscribed", message: "You're already subscribed!" }, { status: 409 }, ); }
const { error } = await createContact({ email, source: "newsletter", subscribeToNewsletter: true, });
if (error) { // Return actual error, not generic 500 const message = typeof error === "object" && "message" in error ? (error as { message: string }).message : "Failed to subscribe"; const statusCode = typeof error === "object" && "statusCode" in error ? (error as { statusCode: number }).statusCode : 500; return NextResponse.json({ error: message }, { status: statusCode }); }
// Send welcome email
await resend.emails.send({
from: "Company <noreply@example.com>",
to: [email],
subject: "Welcome to our Newsletter",
html: <h2>Thanks for subscribing!</h2>...,
});
return NextResponse.json({ success: true }); }
- Frontend Duplicate Handling
const response = await fetch("/api/newsletter", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email }), });
const data = await response.json();
if (response.status === 409) { toast.info("You're already subscribed!"); return; }
if (!response.ok) { throw new Error(data.error); }
toast.success("Thanks for subscribing!");
- Booking/Contact Form (Create Lead)
Add contact creation without blocking the main flow:
// In booking or contact form API route createContact({ email, firstName, lastName, company, source: "booking", // or "contact" }).catch((err) => console.error("Failed to create contact:", err));
- Inbound Email Forwarding
For receiving emails via subdomain (e.g., mail.example.com ):
Webhook handler (/api/webhooks/resend ):
case "email.received": const forwardTo = process.env.EMAIL_FORWARD_TO?.split(",").map(e => e.trim());
if (!forwardTo?.length) return;
await resend.emails.send({
from: "Forwarded <forwarded@example.com>",
to: forwardTo,
replyTo: event.data.from,
subject: [Fwd] ${event.data.subject},
html: <div style="padding: 16px; background: #f5f5f5;"> <p><strong>From:</strong> ${event.data.from}</p> <p><strong>To:</strong> ${event.data.to?.join(", ")}</p> </div> <hr/> ${event.data.html || event.data.text} ,
attachments: event.data.attachments,
});
break;
Environment Variables
Required
RESEND_API_KEY=re_xxxxx
Optional - for Audiences integration
RESEND_SEGMENT_NEWSLETTER=seg_xxxxx RESEND_SEGMENT_LEADS=seg_xxxxx RESEND_TOPIC_NEWSLETTER=top_xxxxx
Optional - for email forwarding
EMAIL_FORWARD_TO=email1@example.com,email2@example.com
Resend Dashboard Setup
IMPORTANT: Create these in the dashboard BEFORE deploying code that uses them.
Create Properties
Properties must exist before the API can use them.
-
Go to Audiences → Properties tab
-
Create these properties:
-
domain (text) - For multi-domain account filtering
-
source (text) - How contact signed up (newsletter, booking, contact)
-
company (text) - Optional company name
Create Segments
-
Go to Audiences → Segments
-
Create "project-newsletter" segment
-
Create "project-leads" segment
-
Copy IDs to env vars
Create Topics
-
Go to Audiences → Topics
-
Create topic (e.g., "Project Newsletter")
-
Defaults to: Opt-in (subscribers must explicitly opt in)
-
Visibility: Public (visible on preference page) or Private
-
Copy ID to env var
Email Receiving (Subdomain)
To receive emails without conflicting with existing email (e.g., Google Workspace):
DNS: Add MX record for subdomain
-
Name: mail
-
Content: inbound-smtp.us-east-1.amazonaws.com
-
Priority: 10
Resend: Enable receiving for mail.yourdomain.com
Webhook: Point to your /api/webhooks/resend endpoint
Broadcasts
Use Resend dashboard for sending newsletters:
-
Go to Broadcasts → Create
-
Select segment to target
-
Use personalization: {{{FIRST_NAME|there}}}
-
Include unsubscribe: {{{RESEND_UNSUBSCRIBE_URL}}}
-
Send or schedule
Common Patterns
Sender Addresses
Use consistent from addresses:
-
Automated notifications
-
Contact form
-
Calendar invites
-
Forwarded inbound emails
Team Notifications
Send internal notifications to a subdomain address that forwards:
to: ["info@mail.domain.com"] // Forwards via webhook