Resend: Transactional Email Done Right in 2026

Production-ready guide to Resend's email API. Error patterns, code examples, and why indie hackers are switching from SendGrid.

TL;DR

Resend is a modern transactional email service built for developers. We cover setup, common errors, production patterns, and why it's gaining traction in the indie hacker community.

---

What Changed in 2026?

Resend has solidified its position as the "developer-first" email solution. While SendGrid still dominates enterprise, Resend's focus on simplicity and reasonable pricing ($20-200/mo depending on volume) makes it the default choice for bootstrapped products.

Current status (verify in [official docs](https://resend.com/docs)): The API remains stable, with recent additions around batch sending and webhook reliability improvements.

---

Why Resend Over Alternatives?

The indie hacker calculus:

  • No monthly minimum commitments (pay-per-email after free tier)
  • Email templates with React/JSX (game-changer)
  • Webhook reliability: built-in retry logic with exponential backoff
  • Dashboard that doesn't require a MBA to navigate
  • DKIM/SPF handled automatically
  • Compare this to SendGrid's complexity or Mailgun's dated UX, and the choice clarifies fast.

    ---

    Getting Started: The Right Way

    Installation

    ```bash npm install resend ```

    Current version (verify in official docs): ^3.0.0+ is standard for new projects as of early 2026.

    First Production Call

    This pattern should be your template:

    ```typescript import { Resend } from 'resend';

    const resend = new Resend(process.env.RESEND_API_KEY);

    export async function sendWelcomeEmail(email: string, userName: string) { try { const { data, error } = await resend.emails.send({ from: 'onboarding@yourdomain.com', to: email, subject: 'Welcome aboard!', html: <h1>Hi ${userName}!</h1><p>You're in.</p>, // Always set reply-to for customer expectations reply_to: 'support@yourdomain.com', // Track engagement (optional but recommended) tags: [{ name: 'category', value: 'welcome' }], });

    if (error) { console.error('Resend error:', error.message); throw new Error(Email send failed: ${error.message}); }

    // Store the message ID for webhook reconciliation await db.emailLog.create({ messageId: data.id, email, type: 'welcome', sentAt: new Date(), });

    return { success: true, messageId: data.id }; } catch (err) { console.error('Unexpected error sending email:', err); // Alert ops - this shouldn't happen await alertSlack(Email send error for ${email}); throw err; } } ```

    ---

    Console Errors You'll See (And How to Fix Them)

    Error 1: Invalid From Address

    ``` Resend error: Invalid from address. Please use a verified domain. Cause: You're sending from an unverified domain Fix: Add your domain in Resend dashboard > Domains > Add Domain, then verify DNS records ```

    Error 2: Rate Limiting

    ``` { "message": "Too many requests", "error": "rate_limit_exceeded" } Cause: Hitting API rate limits (typically 100 req/sec) Fix: Implement queue-based sending with Bull/BullMQ for batch operations ```

    Error 3: Missing API Key

    ``` Error: The RESEND_API_KEY environment variable is missing Cause: Not set in .env.local or deployment config Fix: Copy from Resend dashboard > API Keys > ensure it's in your runtime environment ```

    ---

    React Email Templates: The Secret Weapon

    This is where Resend shines. Use [React Email](https://react.email) to write templates as components:

    ```typescript import { Button, Container, Head, Html, Img, Link, Preview, Row, Section, Text } from '@react-email/components';

    interface InvoiceEmailProps { invoiceNumber: string; amount: number; customerName: string; }

    export function InvoiceEmail({ invoiceNumber, amount, customerName, }: InvoiceEmailProps) { return ( <Html> <Head /> <Preview>Invoice {invoiceNumber} for ${amount}</Preview> <Body style={main}> <Container style={container}> <Section> <Text style={heading}>Invoice {invoiceNumber}</Text> <Text>Hi {customerName},</Text> <Text>Your invoice total: <strong>${amount.toFixed(2)}</strong></Text> <Button style={button} href="https://yourdomain.com/invoice/{invoiceNumber}"> View Invoice </Button> </Section> </Container> </Body> </Html> ); } ```

    Send it:

    ```typescript const { data, error } = await resend.emails.send({ from: 'billing@yourdomain.com', to: customerEmail, subject: Invoice ${invoiceNumber}, react: <InvoiceEmail invoiceNumber={invoiceNumber} amount={amount} customerName={name} />, }); ```

    Why this matters: Type-safe emails, version control your templates, no separate template language to learn.

    ---

    Webhook Handling for Production

    Don't assume emails delivered. Listen for events:

    ```typescript // pages/api/webhooks/resend.ts (Next.js) import { WebhookEvent } from 'resend'; import crypto from 'crypto';

    export async function POST(req: Request) { const signature = req.headers.get('resend-signature'); const body = await req.text();

    // Verify signature (CRITICAL for security) const hash = crypto .createHmac('sha256', process.env.RESEND_WEBHOOK_SECRET!) .update(body) .digest('base64');

    if (signature !== hash) { return new Response('Unauthorized', { status: 401 }); }

    const event: WebhookEvent = JSON.parse(body);

    switch (event.type) { case 'email.delivered': await db.emailLog.update( { messageId: event.data.email_id }, { status: 'delivered', deliveredAt: new Date() } ); break; case 'email.bounced': // Add to suppression list immediately await db.suppressedEmails.create({ email: event.data.email_address }); console.warn(Email bounced: ${event.data.email_address}); break; case 'email.complained': // User marked as spam - handle accordingly await alertSlack(Spam complaint: ${event.data.email_address}); break; }

    return new Response('OK', { status: 200 }); } ```

    ---

    Pricing Reality Check

  • Free tier: 100 emails/day (fine for MVPs)
  • Pay-as-you-go: $0.20 per 1,000 emails (~$0.0002 each)
  • No hidden fees or commitment
  • At 100k emails/month: ~$20. At 1M/month: ~$200. Scale the cost linearly without negotiating contracts.

    ---

    Production Checklist

  • [ ] Domain verified with SPF/DKIM/CNAME records
  • [ ] API key stored in secrets manager (not .env)
  • [ ] Webhook endpoint secured with signature verification
  • [ ] Message IDs logged to database for debugging
  • [ ] Suppression list implemented for bounces
  • [ ] Rate limiting handled (queue-based if high volume)
  • [ ] Reply-to address set for all emails
  • [ ] Tags applied for analytics/filtering
  • [ ] Monitoring on webhook failures
  • [ ] Test email sent from production domain before launch
  • ---

    Related Guides

  • [Email validation best practices](/?guide=email-validation)
  • [Building reliable job queues](/?guide=job-queues)
  • ---

    What am I missing?

    Resend is actively developed and the ecosystem shifts. Tell us in the comments:

  • New features since early 2026 we should highlight?
  • Common gotchas you hit in production?
  • Comparison scenarios we didn't cover?
  • Pricing changes or tier updates?
  • We update based on reader feedback.

    πŸ”₯ 0d
    LIVE
    ↓ PlanetScale rage spiking β€’βš  Vercel pricing complaints ‒↑ Railway gaining fast ‒↑ Supabase happiness rising ‒↑ Resend loved by devs ‒↓ PlanetScale rage spiking β€’βš  Vercel pricing complaints ‒↑ Railway gaining fast ‒↑ Supabase happiness rising
    DEVELOPER PAIN RADAR // Loading...

    Developers complain.
    Opportunities appear.

    We track what developers are struggling with today β€” and what opportunities that creates.

    β€”
    guides today
    β€”
    avg happiness
    πŸ”₯ Pain
    πŸ“– Guides
    πŸ”­ Explore
    πŸ‘€ Mine
    πŸ”₯ Pain Radar β€” rage scores today
    β†— share
    πŸ’‘ Opportunity Feed β€” pain = market gap
    πŸ“ˆ Tool Momentum
    all scores β†’
    πŸ“– Latest Guide
    all guides β†’
    πŸ“– All Guides
    πŸ“Š Tool Scores
    + Submit
    πŸ“° Hacker News
    βž• Submit a Tool
    ← back