How to Send Transactional Emails with Resend 2026
How to Send Transactional Emails with Resend
Resend is the modern developer-first email API. Built by the creators of React Email, it's designed for developers who want great deliverability without the complexity of legacy platforms. This guide covers everything from first email to production templates.
What You'll Build
- Send transactional emails (welcome, receipt, password reset)
- React Email templates with Resend
- Domain verification for production sending
- Webhook tracking for delivery events
- Batch sending and attachments
Prerequisites: Node.js 18+, Resend account (free tier: 3,000 emails/month).
1. Setup
Install
npm install resend
Initialize
// lib/resend.ts
import { Resend } from 'resend';
export const resend = new Resend(process.env.RESEND_API_KEY);
Environment Variables
# .env.local
RESEND_API_KEY=re_...
2. Send Your First Email
const { data, error } = await resend.emails.send({
from: 'Your App <hello@yourdomain.com>',
to: 'user@example.com',
subject: 'Welcome to Your App!',
html: '<h1>Welcome!</h1><p>Thanks for signing up.</p>',
});
if (error) {
console.error('Failed to send:', error);
} else {
console.log('Email sent:', data.id);
}
API Route (Next.js)
// app/api/send-email/route.ts
import { NextResponse } from 'next/server';
import { resend } from '@/lib/resend';
export async function POST(req: Request) {
const { to, name } = await req.json();
const { data, error } = await resend.emails.send({
from: 'Your App <hello@yourdomain.com>',
to,
subject: `Welcome, ${name}!`,
html: `
<h1>Welcome to Your App, ${name}!</h1>
<p>Your account is ready. Here's what to do next:</p>
<ol>
<li>Complete your profile</li>
<li>Explore the dashboard</li>
<li>Invite your team</li>
</ol>
<a href="https://yourapp.com/dashboard">Go to Dashboard →</a>
`,
});
if (error) {
return NextResponse.json({ error }, { status: 500 });
}
return NextResponse.json({ id: data!.id });
}
3. React Email Templates
React Email lets you build email templates with React components — version-controlled, testable, and type-safe.
Install React Email
npm install @react-email/components
Create a Template
// emails/WelcomeEmail.tsx
import {
Body,
Container,
Head,
Heading,
Html,
Link,
Preview,
Section,
Text,
Button,
} from '@react-email/components';
interface WelcomeEmailProps {
name: string;
dashboardUrl: string;
}
export function WelcomeEmail({ name, dashboardUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>Welcome to Your App, {name}!</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={heading}>Welcome, {name}!</Heading>
<Text style={text}>
Your account is ready. Click below to get started.
</Text>
<Section style={buttonSection}>
<Button style={button} href={dashboardUrl}>
Go to Dashboard
</Button>
</Section>
<Text style={footer}>
Questions? Reply to this email or visit our{' '}
<Link href="https://yourapp.com/help">help center</Link>.
</Text>
</Container>
</Body>
</Html>
);
}
const main = { backgroundColor: '#f6f9fc', fontFamily: 'sans-serif' };
const container = { margin: '0 auto', padding: '40px 20px', maxWidth: '560px' };
const heading = { fontSize: '24px', fontWeight: 'bold', color: '#1a1a1a' };
const text = { fontSize: '16px', color: '#4a4a4a', lineHeight: '24px' };
const buttonSection = { textAlign: 'center' as const, margin: '32px 0' };
const button = {
backgroundColor: '#2563eb',
color: '#ffffff',
padding: '12px 24px',
borderRadius: '6px',
fontSize: '16px',
textDecoration: 'none',
};
const footer = { fontSize: '14px', color: '#8a8a8a' };
Send with React Email Template
import { resend } from '@/lib/resend';
import { WelcomeEmail } from '@/emails/WelcomeEmail';
const { data, error } = await resend.emails.send({
from: 'Your App <hello@yourdomain.com>',
to: 'user@example.com',
subject: 'Welcome to Your App!',
react: WelcomeEmail({
name: 'Alex',
dashboardUrl: 'https://yourapp.com/dashboard',
}),
});
4. Domain Verification
To send from your own domain (not onboarding@resend.dev), verify your domain:
Add DNS Records
Resend requires these DNS records:
| Type | Name | Value | Purpose |
|---|---|---|---|
| TXT | @ | v=spf1 include:resend.com ~all | SPF (sender authorization) |
| CNAME | resend._domainkey | resend.domainkey... | DKIM (email signing) |
| TXT | _dmarc | v=DMARC1; p=none; | DMARC (email policy) |
Verify via API
// Check domain verification status
const domains = await resend.domains.list();
console.log(domains);
// Or add a new domain
const domain = await resend.domains.create({
name: 'yourdomain.com',
});
5. Common Email Types
Password Reset
await resend.emails.send({
from: 'Security <security@yourdomain.com>',
to: userEmail,
subject: 'Reset your password',
react: PasswordResetEmail({
resetUrl: `https://yourapp.com/reset?token=${token}`,
expiresIn: '1 hour',
}),
});
Order Confirmation
await resend.emails.send({
from: 'Orders <orders@yourdomain.com>',
to: customerEmail,
subject: `Order #${orderId} confirmed`,
react: OrderConfirmationEmail({
orderId,
items,
total,
estimatedDelivery,
}),
});
Team Invitation
await resend.emails.send({
from: 'Your App <team@yourdomain.com>',
to: inviteeEmail,
subject: `${inviterName} invited you to join ${teamName}`,
react: TeamInviteEmail({
inviterName,
teamName,
inviteUrl: `https://yourapp.com/invite?token=${token}`,
}),
});
6. Batch Sending
Send multiple emails in one API call:
const { data, error } = await resend.batch.send([
{
from: 'App <hello@yourdomain.com>',
to: 'user1@example.com',
subject: 'Your weekly summary',
react: WeeklySummaryEmail({ userId: '1' }),
},
{
from: 'App <hello@yourdomain.com>',
to: 'user2@example.com',
subject: 'Your weekly summary',
react: WeeklySummaryEmail({ userId: '2' }),
},
]);
Limits: Up to 100 emails per batch call.
7. Webhooks
Track email delivery events:
Register Webhook
// In Resend dashboard or via API
await resend.webhooks.create({
url: 'https://yourapp.com/api/webhooks/resend',
events: [
'email.sent',
'email.delivered',
'email.bounced',
'email.complained',
'email.opened',
'email.clicked',
],
});
Handle Webhook Events
// app/api/webhooks/resend/route.ts
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
const event = await req.json();
switch (event.type) {
case 'email.delivered':
// Update delivery status in database
await markEmailDelivered(event.data.email_id);
break;
case 'email.bounced':
// Mark email as invalid, stop sending
await handleBounce(event.data.to, event.data.bounce_type);
break;
case 'email.complained':
// User marked as spam — unsubscribe immediately
await unsubscribeUser(event.data.to);
break;
}
return NextResponse.json({ received: true });
}
Pricing
| Tier | Emails/Month | Price |
|---|---|---|
| Free | 3,000 | $0 |
| Pro | 50,000 | $20/month |
| Scale | 100,000 | $90/month |
| Enterprise | Custom | Custom |
No per-email overage charges on free tier — sending simply stops at the limit.
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Sending from unverified domain | Emails go to spam | Verify domain with SPF/DKIM/DMARC |
| No bounce handling | Hurts sender reputation | Handle email.bounced webhooks |
| HTML emails without plain text fallback | Some clients show blank | Provide text alongside html |
| Hardcoding "from" address | Can't change sender easily | Use environment variable |
| Not testing email rendering | Looks broken in Outlook | Preview in React Email dev server |
Deliverability Best Practices
A transactional email API handles the mechanics of sending — SMTP connections, retries, bounce handling at the infrastructure level — but deliverability depends on your sending behavior, not just the platform. Gmail, Outlook, and Apple Mail run incoming email through spam scoring algorithms that consider your domain's reputation, sending volume patterns, and user engagement signals.
Domain reputation is per-IP and per-domain. When you first add a domain to Resend and start sending, you're building reputation from scratch. ISPs (Gmail in particular) watch your initial sends carefully — high bounce rates or spam complaints in the first few weeks can damage your domain's reputation permanently with some ISPs. Best practices for a new sending domain: start with small volumes (under 1,000 emails/day for the first week), send only to engaged users (recent signups, not old lists), and monitor your bounce and complaint rates in the Resend dashboard.
List hygiene is the biggest lever on deliverability. Every email you send to an invalid address returns a hard bounce; every email a user marks as spam generates a complaint. Email providers track these rates: Gmail's spam threshold is roughly 0.10% (1 complaint per 1,000 emails) and will start filtering your email to spam if you exceed it. Above 0.30%, delivery stops entirely. Keep your list clean by removing addresses that haven't engaged in 6 months, and never import purchased or scraped lists — they're full of invalid addresses and spam traps that will destroy your sending reputation instantly.
Engagement signals matter. Gmail uses open rates and click rates as positive signals. Sending to users who never open your emails trains Gmail's algorithm to classify your emails as low-priority. Segment your list and suppress long-inactive subscribers before sending: if a user hasn't opened any email in 12 months, remove them from marketing sends entirely. For transactional emails (password resets, order confirmations), this doesn't apply — those should always be sent regardless of engagement history.
Warm up dedicated IPs carefully. On Resend's Scale plan and above, you can request dedicated sending IPs. These give you full control over your sending reputation but start with zero reputation — ISPs don't know this IP yet. Warm up by sending small volumes initially (500/day week 1, doubling each week) and only sending to your most engaged users during the warmup period. Resend provides guidance on IP warmup schedules in their documentation.
Queuing and Retry Logic
The Resend API returns synchronous errors when the send fails immediately (network issues, invalid API key, malformed payload) but delivery failures (bounces, spam blocks) arrive via webhooks minutes or hours later. A production email system needs both: a queue for rate limiting and retry, and webhook handling for delivery status.
Never send email directly from a user request. If the Resend API is slow or returns a transient error, you don't want the user waiting or seeing an error. Instead, write the email to a database queue and return success to the user immediately. A background worker sends from the queue:
// On user action — add to queue:
await db.emailQueue.create({
data: {
to: user.email,
type: 'welcome',
payload: JSON.stringify({ name: user.name }),
status: 'pending',
scheduledAt: new Date(),
attempts: 0,
},
});
// Background worker (runs every 30 seconds):
async function processEmailQueue() {
const pending = await db.emailQueue.findMany({
where: {
status: 'pending',
scheduledAt: { lte: new Date() },
attempts: { lt: 3 },
},
take: 50, // Process in batches of 50
});
for (const job of pending) {
try {
await db.emailQueue.update({
where: { id: job.id },
data: { status: 'sending', attempts: { increment: 1 } },
});
const { data, error } = await resend.emails.send({
from: 'App <hello@yourapp.com>',
to: job.to,
subject: getSubject(job.type),
react: getTemplate(job.type, JSON.parse(job.payload)),
});
if (error) throw new Error(error.message);
await db.emailQueue.update({
where: { id: job.id },
data: { status: 'sent', resendId: data!.id, sentAt: new Date() },
});
} catch (err) {
const nextAttempt = job.attempts >= 2
? new Date(Date.now() + 60 * 60 * 1000) // 1 hour delay on final retry
: new Date(Date.now() + 5 * 60 * 1000); // 5 min delay on first retries
await db.emailQueue.update({
where: { id: job.id },
data: {
status: job.attempts >= 2 ? 'failed' : 'pending',
scheduledAt: nextAttempt,
error: String(err),
},
});
}
}
}
Resend's API rate limits: the free tier allows 2 requests/second; paid plans allow up to 10 requests/second. For batch sends to many users (weekly digests, announcements), use resend.batch.send() to send up to 100 emails per API call rather than 100 individual calls. The queue pattern above with take: 50 combined with batch sending gives you 5,000 emails per second at rate limit, which handles most production volumes.
Testing Email Templates
React Email provides a development server that renders your email templates in a browser, making it easy to iterate on design before sending real emails. Run it alongside your Next.js app:
npx react-email dev --dir emails --port 3001
This starts a preview server at localhost:3001 with hot reload for all templates in your emails/ directory. Each template renders as it would in email clients, with a preview text bar and basic responsiveness.
Email client compatibility testing is more involved. React Email components are designed to output HTML compatible with Outlook, Gmail, Apple Mail, and mobile clients, but you should test real renders before launching. Email on Acid and Litmus both offer preview rendering across 50+ client and device combinations. For critical email templates (password reset, receipt), test across at least Gmail (web), Outlook 365, and iOS Mail before shipping.
Inline CSS vs. style sheets: email clients notoriously strip <style> tags from <head>. React Email handles this automatically by inlining styles on each element, but if you're writing raw HTML templates, you must inline styles yourself or use an inliner library like juice. The React Email components (Container, Button, Text, etc.) all use inline styles internally, which is why they render correctly across clients.
Dark mode support is inconsistent across email clients. Gmail on Android and iOS renders dark mode by inverting colors. Outlook has its own dark mode behavior. React Email's components don't include dark mode CSS by default — if dark mode matters for your brand, use media queries in a <Head> component and test in Litmus or Email on Acid which simulates dark mode rendering.
Testing webhook delivery locally: use the Resend webhook preview feature in your dashboard, or set up a local tunnel with ngrok and point your webhook URL to https://your-ngrok-id.ngrok.io/api/webhooks/resend. Send test emails through the Resend dashboard and verify your webhook handler processes all event types correctly before deploying to production.
Methodology
Resend is one of several modern transactional email APIs targeting developers — Postmark, Mailgun, and SendGrid are the main alternatives. Resend's key differentiators are the React Email integration (which makes template development significantly faster than HTML string templates), the developer experience (clear error messages, minimal setup), and transparent pricing without per-email fees beyond the plan limit. Postmark has historically stronger deliverability for high-volume transactional email but a higher price floor. SendGrid is more complex but offers marketing email and advanced analytics on the same platform. For most new projects in 2026, Resend is the fastest path from zero to production email delivery.
Resend SDK examples use resend npm package v3.x. React Email component examples use @react-email/components v0.0.x. Email authentication DNS record values (SPF, DKIM) are illustrative — use the exact records provided in your Resend dashboard during domain setup, as the DKIM CNAME record value is unique to your domain. Deliverability thresholds (0.10% spam complaint rate for Gmail) sourced from Google's Email Sender Guidelines published February 2024 and effective February 2024; verify current thresholds from Google's Postmaster Tools documentation. Resend pricing verified from published pricing page as of March 2026; free tier (3,000 emails/month) and batch API limit (100 emails/call) verified from Resend API documentation. Rate limits (2 req/sec free, 10 req/sec paid) from Resend's rate limiting documentation. SMTP deliverability best practices follow M3AAWG (Messaging, Malware and Mobile Anti-Abuse Working Group) guidelines and RFC 5321.
Choosing an email API? Compare Resend vs SendGrid vs Postmark on APIScout — pricing, deliverability, and developer experience.
Related: Building a SaaS Backend, How to Build Email Templates with React Email + Resend, Building a Communication Platform