Skip to content
resend-integration logo

Resend Integration

resend-integration

Load when integrating Resend for transactional email with React Email templates. Applies when implementing email sending, webhook handling, domain verification, or batch email operations.

telum-ai/speck0installs2starsMarketing

SKILL.md

Full skill instructions

When This Rule Applies

Apply when implementing transactional email, marketing email, or any email sending with Resend.


React Email Templates

Template Component

// emails/​order-confirmation.tsx
import { Html, Container, Heading, Text, Button } from '@react-email/​components';

interface OrderProps {
  orderNumber: string;
  customerName: string;
  totalAmount: number;
}

export function OrderConfirmationEmail({ orderNumber, customerName, totalAmount }: OrderProps) {
  return (
    <Html lang="en">
      <Container style={{ maxWidth: '600px', margin: '0 auto' }}>
        <Heading>Thank you, {customerName}!</​Heading>
        <Text>Order #{orderNumber} confirmed.</​Text>
        <Text style={{ fontWeight: 'bold' }}>Total: ${totalAmount.toFixed(2)}</​Text>
        <Button href={`https://yourapp.com/​orders/​${orderNumber}`}>
          View Order
        </​Button>
      </​Container>
    </​Html>
  );
}

Sending with React Component

import { Resend } from 'resend';
import { OrderConfirmationEmail } from './​emails/​order-confirmation';

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

await resend.emails.send({
  from: '[email protected]',
  to: customerEmail,
  subject: `Order Confirmation: #${orderNumber}`,
  react: <OrderConfirmationEmail {...props} />,
});

Webhook Handling

Next.js Webhook Endpoint

// app/​api/​webhooks/​resend/​route.ts
import { NextRequest, NextResponse } from 'next/​server';
import crypto from 'crypto';

export async function POST(request: NextRequest) {
  const signature = request.headers.get('x-resend-signature');
  const rawBody = await request.text();
  
  // Validate signature
  if (!validateSignature(rawBody, signature!, process.env.RESEND_WEBHOOK_SECRET!)) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }
  
  const payload = JSON.parse(rawBody);
  
  switch (payload.type) {
    case 'email.delivered':
      await handleDelivered(payload.data);
      break;
    case 'email.bounced':
      await handleBounce(payload.data); // Remove from mailing list
      break;
    case 'email.complained':
      await handleComplaint(payload.data); // Unsubscribe user
      break;
  }
  
  return NextResponse.json({ success: true });
}

function validateSignature(payload: string, signature: string, secret: string): boolean {
  const hash = crypto.createHmac('sha256', secret).update(payload).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(signature));
}

Webhook Event Types

EventWhenAction
email.sentAPI request successfulLog attempt
email.deliveredReached recipient serverMark delivered
email.bouncedDelivery failedRemove from list
email.complainedMarked as spamUnsubscribe
email.delivery_delayedTemporary issueMonitor

Error Handling & Retries

Retry with Exponential Backoff

async function sendEmailWithRetry(params: EmailParams, maxAttempts = 5) {
  let delay = 500;
  
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await resend.emails.send(params);
    } catch (error) {
      if (attempt === maxAttempts) throw error;
      
      // Don't retry auth/​validation errors
      if (isNonRetriable(error)) throw error;
      
      const jitter = Math.random() * delay * 0.1;
      await sleep(delay + jitter);
      delay = Math.min(delay * 2, 30000);
    }
  }
}

Idempotency Keys (Prevent Duplicates)

// Use deterministic key based on event + entity
const idempotencyKey = `order-confirmation/​${orderId}`;

await resend.emails.send(
  { from, to, subject, react: <Template /> },
  { idempotencyKey }
);

// Same key + same payload = no duplicate send
// Same key + different payload = error (intentional)

Domain Verification

Required DNS Records

RecordTypeNameValue
SPFTXTsendv=spf1 include:amazonses.com ~all
DKIMTXTresend._domainkeyv=DKIM1; h=sha256; p=...
MXMXinboundinbound-smtp.us-east-1.amazonaws.com

Best Practice: Use Subdomains

// Use notifications.yourdomain.com instead of yourdomain.com
// - Isolates sending reputation
// - Makes intent clear to recipients
// - Reduces impact of deliverability issues

Disable Tracking for Transactional Emails

Open/​click tracking can trigger spam filters. Disable for critical emails:

await resend.domains.create({
  name: 'notifications.yourdomain.com',
  // Disable tracking for better deliverability
  openTracking: false,
  clickTracking: false,
});

Batch Sending

Send Up to 100 Emails Per Request

const emails = recipients.map(recipient => ({
  from: '[email protected]',
  to: recipient.email,
  subject: 'Newsletter',
  html: generateHTML(recipient.name),
}));

// Batch send (100 max per call)
const response = await resend.batch.send(emails);

Rate Limiting

Default: 2 requests/​second. Check headers:

// Response headers
'ratelimit-limit': 2
'ratelimit-remaining': 1
'ratelimit-reset': 5
'retry-after': 5  // When rate limited

Common Gotchas

Webhook Duplicates

Resend uses "at least once" delivery. Implement idempotency:

const processed = await db.findEvent(webhookId);
if (processed) return; // Skip duplicate
await processWebhook(payload);
await db.markProcessed(webhookId);

60-Second Webhook Timeout

Return 200 immediately, process async:

await jobQueue.enqueue('processEmail', payload);
return NextResponse.json({ success: true }); // Return fast

Domain Not Verified

  • DNS propagation takes up to 24 hours
  • Use exact record values from Resend dashboard
  • Check for typos in record names

Emails Going to Spam

  • Enable SPF + DKIM (both required)
  • Disable tracking for transactional emails
  • Use consistent "from" address
  • Warm up new domains gradually

Quick Reference

TaskPattern
Send with React templatereact: <Component />
Prevent duplicates{ idempotencyKey } option
Batch sendresend.batch.send(emails)
Validate webhooksCheck x-resend-signature header
Handle bouncesWebhook → remove from list

References