Resend: Transactional Email Done Right in 2026
Production-ready patterns for Resend v3+. Real error messages, exact setup, and patterns that actually scale for indie hackers.
TL;DR
Resend (v3.0+) eliminates SMTP complexity for transactional email. This guide covers production patterns, actual error messages you'll hit, and why it beats traditional email services for indie projects. [Verify current pricing and API limits in official docs](https://resend.com/docs).
---
Why Resend Matters for Indie Hackers
Traditional transactional email via SendGrid, Mailgun, or AWS SES requires:
Resend abstracts this away. You get a clean REST API, built-in React email templates (React Email v0.0.x), and deliverability that "just works" for most indie projects.
Verify in official docs: Current pricing tier limits—free tier includes 100 emails/day, paid starts at $20/month for higher volume.
---
Core Setup (v3.0+)
Installation
```bash npm install resend
or
pip install resend # Python SDK v0.11.0+ ```Basic Authentication
```typescript import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
// Verify your API key isn't leaking if (!process.env.RESEND_API_KEY) { throw new Error('RESEND_API_KEY is required'); } ```
Error Message #1 - You'll see this immediately:
``` UnauthorizedError: Unauthorized. Invalid API key at /node_modules/resend/dist/index.js:145:23 ```
Fix: Check your .env.local file. API keys are case-sensitive. Rotate your key in the [Resend dashboard](https://resend.com/api-keys) if compromised.
---
Production Pattern: React Email Templates
Resend's killer feature is React Email integration. Instead of string concatenation or EJS templates, write JSX:
```typescript import * as React from 'react'; import { Html, Body, Container, Heading, Text, Link } from 'react-email';
interface WelcomeEmailProps { userEmail: string; activationLink: string; }
export const WelcomeEmail = ({ userEmail, activationLink }: WelcomeEmailProps) => ( <Html> <Body style={bodyStyle}> <Container style={containerStyle}> <Heading style={headingStyle}>Welcome!</Heading> <Text style={textStyle}>We're thrilled to have {userEmail}.</Text> <Link href={activationLink} style={buttonStyle} > Activate Account </Link> </Container> </Body> </Html> );
const bodyStyle = { fontFamily: 'sans-serif', backgroundColor: '#f5f5f5' }; const containerStyle = { maxWidth: '600px', margin: '0 auto', padding: '20px' }; const headingStyle = { fontSize: '24px', marginBottom: '16px' }; const textStyle = { fontSize: '14px', lineHeight: '1.6', color: '#333' }; const buttonStyle = { display: 'inline-block', backgroundColor: '#000', color: '#fff', padding: '12px 24px', borderRadius: '4px', textDecoration: 'none', }; ```
Then send it:
```typescript
const { data, error } = await resend.emails.send({
from: 'onboarding@yourdomain.com', // Verify domain ownership first
to: userEmail,
subject: 'Welcome to StillNotAThing',
react: (
<WelcomeEmail
userEmail={userEmail}
activationLink={https://yourdomain.com/activate?token=${activationToken}}
/>
),
});
if (error) {
console.error('Email send failed:', error);
throw new Error(Failed to send welcome email: ${error.message});
}
console.log('Email sent:', data.id); // Save this ID for bounce tracking ```
Error Message #2 - Domain verification:
``` ValidationError: Invalid "from" email address. "onboarding@yourdomain.com" does not have a registered domain. at POST /emails [v1/send] ```
Fix: Add your domain in Resend dashboard → Domains. You'll need to verify DNS records (DKIM, SPF). This takes 5-10 minutes.
---
Handling Bounces and Webhooks
Resend sends webhook events for bounces, complaints, and delivery. Store the email ID from the response:
```typescript interface EmailRecord { id: string; // Resend's email ID to: string; templateUsed: string; sentAt: Date; }
// Save to your database await db.emailLogs.create({ resendId: data.id, to: userEmail, templateUsed: 'welcome', sentAt: new Date(), }); ```
Then set up a webhook endpoint:
```typescript // pages/api/resend-webhook.ts import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler( req: NextApiRequest, res: NextApiResponse ) { if (req.method !== 'POST') return res.status(405).end();
const { type, data } = req.body;
if (type === 'email.bounced') { const emailRecord = await db.emailLogs.findBy({ resendId: data.email_id }); await db.users.update(emailRecord.userId, { emailBounced: true }); }
if (type === 'email.complained') { // Handle spam complaints await flagUserForReview(data.email); }
res.json({ received: true }); } ```
Error Message #3 - Invalid webhook signature:
``` Error: Invalid signature. Webhook verification failed. at verifySignature(req.headers['x-resend-signature']) ```
Fix: [Verify webhook signatures](https://resend.com/docs/webhooks) using the signing secret from your dashboard.
---
Batch Sending Pattern
For newsletters or bulk notifications, use batch API:
```typescript const users = await db.users.find({ subscribed: true });
const { data, error } = await resend.batch.send([ ...users.map(user => ({ from: 'newsletter@yourdomain.com', to: user.email, subject: 'Weekly Digest', react: <NewsletterEmail digest={weeklyDigest} />, })), ]);
// Returns array of { id, error? } objects
data.forEach(result => {
if (result.error) {
console.warn(Failed for user: ${result.error.message});
}
});
```
Performance: Batch sends are rate-limited—[verify current limits in official docs](https://resend.com/docs/api-reference/batch/send).
---
Testing Locally
Resend provides sandbox mode. Use resend_test_* API keys—emails don't actually send, but API responses are valid:
```typescript const resend = new Resend(process.env.RESEND_API_KEY || 'resend_test_abc123');
// Safe in development—no real emails sent const { data } = await resend.emails.send({ from: 'test@example.com', to: 'developer@example.com', react: <TestEmail />, }); ```
---
Related Patterns
Learn more about email workflows:
---
What am I missing?
This guide covers v3.0+ patterns as of early 2026. If you're using a different version, seeing errors not listed here, or have production patterns that saved your project—drop them in comments. Did I miss:
Let's build this together.