Claude Skill

api-email-resend-react-email

Resend + React Email templates

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_api-email-resend-react-email_skills_api-email-resend-react-email-3a51ef5.zip · 20 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-email-resend-react-email/skills/api-email-resend-react-email
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Email Patterns with Resend and React Email

Quick Guide: Use Resend for transactional emails with React Email templates. Always await render() before sending (it returns a Promise). Server-side only - never expose API keys to clients. Implement retry with exponential backoff for transient failures. Include unsubscribe links in non-transactional emails (CAN-SPAM). Use resend.batch.send() for 2-100 recipients (no attachments or scheduling support in batch). React Email 5.0+ deprecated renderAsync - use render() instead. Webhook verification requires raw request body and webhookSecret parameter.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST await render() before passing HTML to resend.emails.send() - render returns a Promise)

(You MUST handle Resend API errors and implement retry logic for transient failures)

(You MUST use server-side sending for all emails - never expose RESEND_API_KEY to the client)

(You MUST include unsubscribe links in marketing/notification emails - required for CAN-SPAM compliance)

(You MUST use typed props interfaces for all email templates - enables compile-time validation)

</critical_requirements>


Auto-detection: Resend, React Email, @react-email/components, resend.emails.send, email template, transactional email, verification email, password reset email, notification email, email rendering, resend.batch.send, resend.webhooks.verify

When to use:

  • Sending transactional emails (verification, password reset, receipts)
  • Creating React Email templates with Tailwind styling
  • Building notification systems with email delivery
  • Implementing email tracking via webhooks
  • Batch sending to multiple recipients

When NOT to use:

  • Marketing campaign management (use dedicated marketing tools)
  • SMS or push notifications (different services)
  • Email list management (use Resend Audiences or marketing tools)



Detailed Resources:


<red_flags>

RED FLAGS

High Priority Issues:

  • Not awaiting render() - sends "[object Promise]" as email body
  • API key exposed on client - security vulnerability
  • No error handling - silent failures
  • Missing unsubscribe links in non-transactional emails - CAN-SPAM violation
  • Sending without checking user preferences - spam

Medium Priority Issues:

  • No retry logic for transient failures (rate limits, 5xx errors)
  • Sync sending blocking request handlers for non-critical emails
  • Hardcoded from address instead of environment variable
  • No webhook verification signature check
  • Not logging email send results

Common Mistakes:

  • Using Grid or Flexbox in email templates (not supported by email clients)
  • Expecting shadows or gradients to render in emails
  • Using rem units (email clients handle differently)
  • Forgetting PreviewProps for dev server

Gotchas & Edge Cases:

  • Resend SDK accepts a react prop directly (renders internally), but pre-rendering with await render() + html gives you control over the output and works outside the Resend SDK
  • render() is async in React Email 5.0+ (renderAsync deprecated)
  • Batch API limited to 100 emails, does NOT support attachments or scheduledAt
  • Webhooks require raw request body - JSON parsing breaks signature verification
  • Webhook verify uses webhookSecret parameter (not secret)
  • Webhook headers object uses short keys: id, timestamp, signature
  • Idempotency keys are passed as a second argument to resend.emails.send(), not in the email payload headers
  • Idempotency keys expire after 24 hours, max 256 characters
  • Tags: ASCII alphanumeric, underscores, dashes only, max 256 chars per key/value
  • Tailwind in emails requires @react-email/tailwind wrapper (Tailwind 4 supported in React Email 5.0+)
  • Images must use absolute URLs (no relative paths)

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST await render() before passing HTML to resend.emails.send() - render returns a Promise)

(You MUST handle Resend API errors and implement retry logic for transient failures)

(You MUST use server-side sending for all emails - never expose RESEND_API_KEY to the client)

(You MUST include unsubscribe links in marketing/notification emails - required for CAN-SPAM compliance)

(You MUST use typed props interfaces for all email templates - enables compile-time validation)

Failure to follow these rules will cause email delivery failures, security vulnerabilities, or legal compliance issues.

</critical_reminders>


Sources

Files (skills)
  • examples
    • advanced-features.md 6.8 KB
      # Email - Advanced Features Examples
      
      > Scheduled sending, idempotency keys, and tags. See [SKILL.md](../SKILL.md) for core concepts and [core.md](core.md) for basic send pattern.
      
      ---
      
      ## Pattern 1: Scheduled Email Sending
      
      Send emails at a future time using the `scheduledAt` parameter.
      
      ```typescript
      // lib/email/scheduled-email.ts
      import { Resend } from "resend";
      import { render } from "@react-email/components";
      
      const resend = new Resend(process.env.RESEND_API_KEY);
      const MAX_SCHEDULE_DAYS = 30; // Resend allows scheduling up to 30 days in advance
      
      interface ScheduledEmailOptions {
        to: string | string[];
        subject: string;
        react: React.ReactElement;
        scheduledAt: Date;
        tags?: Array<{ name: string; value: string }>;
      }
      
      export async function sendScheduledEmail(
        options: ScheduledEmailOptions,
      ): Promise<{ success: boolean; id?: string; error?: string }> {
        // Validate scheduling window (up to 30 days in advance)
        const now = new Date();
        const maxScheduleDate = new Date(
          now.getTime() + MAX_SCHEDULE_DAYS * 24 * 60 * 60 * 1000,
        );
      
        if (options.scheduledAt <= now) {
          return { success: false, error: "Scheduled time must be in the future" };
        }
      
        if (options.scheduledAt > maxScheduleDate) {
          return {
            success: false,
            error: `Cannot schedule more than ${MAX_SCHEDULE_DAYS} days in advance`,
          };
        }
      
        try {
          const html = await render(options.react);
      
          const { data, error } = await resend.emails.send({
            from: `${process.env.EMAIL_FROM_NAME} <${process.env.EMAIL_FROM_ADDRESS}>`,
            to: options.to,
            subject: options.subject,
            html,
            scheduledAt: options.scheduledAt.toISOString(),
            tags: options.tags,
          });
      
          if (error) {
            console.error("[Email] Scheduled send failed:", error);
            return { success: false, error: error.message };
          }
      
          console.log("[Email] Scheduled:", data?.id, "for", options.scheduledAt);
          return { success: true, id: data?.id };
        } catch (err) {
          const message = err instanceof Error ? err.message : "Unknown error";
          return { success: false, error: message };
        }
      }
      
      export type { ScheduledEmailOptions };
      ```
      
      **Why good:** Validates scheduling window (up to 30 days), converts Date to ISO string for API, supports tags with scheduled emails
      
      ---
      
      ## Pattern 2: Idempotency Keys
      
      Prevent duplicate email sends using idempotency keys.
      
      ```typescript
      // lib/email/idempotent-email.ts
      import { Resend } from "resend";
      import { render } from "@react-email/components";
      
      const resend = new Resend(process.env.RESEND_API_KEY);
      const IDEMPOTENCY_KEY_MAX_LENGTH = 256;
      
      interface IdempotentEmailOptions {
        to: string | string[];
        subject: string;
        react: React.ReactElement;
        idempotencyKey: string;
      }
      
      export async function sendIdempotentEmail(
        options: IdempotentEmailOptions,
      ): Promise<{
        success: boolean;
        id?: string;
        error?: string;
        isDuplicate?: boolean;
      }> {
        if (options.idempotencyKey.length > IDEMPOTENCY_KEY_MAX_LENGTH) {
          return {
            success: false,
            error: `Idempotency key must be ${IDEMPOTENCY_KEY_MAX_LENGTH} characters or less`,
          };
        }
      
        try {
          const html = await render(options.react);
      
          // Idempotency key is passed as a second argument, not in the email payload
          const { data, error } = await resend.emails.send(
            {
              from: `${process.env.EMAIL_FROM_NAME} <${process.env.EMAIL_FROM_ADDRESS}>`,
              to: options.to,
              subject: options.subject,
              html,
            },
            {
              idempotencyKey: options.idempotencyKey,
            },
          );
      
          if (error) {
            const errorName = error.name?.toLowerCase() ?? "";
      
            if (errorName.includes("invalid_idempotent_request")) {
              return {
                success: false,
                error: "This idempotency key was already used with different payload",
                isDuplicate: true,
              };
            }
      
            if (errorName.includes("concurrent_idempotent_requests")) {
              return {
                success: false,
                error: "Another request with this idempotency key is in progress",
                isDuplicate: true,
              };
            }
      
            return { success: false, error: error.message };
          }
      
          return { success: true, id: data?.id };
        } catch (err) {
          const message = err instanceof Error ? err.message : "Unknown error";
          return { success: false, error: message };
        }
      }
      
      export type { IdempotentEmailOptions };
      ```
      
      **Why good:** Validates key length, handles idempotency-specific errors, returns `isDuplicate` flag for caller handling
      
      ---
      
      ## Pattern 3: Usage - Idempotent Order Confirmation
      
      Use order ID as idempotency key to prevent duplicate confirmation emails.
      
      ```typescript
      import { sendIdempotentEmail } from "./lib/email/idempotent-email";
      import { OrderConfirmationEmail } from "./templates/order-confirmation";
      
      async function sendOrderConfirmation(order: {
        id: string;
        userEmail: string;
        userName: string;
        total: number;
      }) {
        // Use order ID as idempotency key - guarantees one email per order
        const result = await sendIdempotentEmail({
          to: order.userEmail,
          subject: `Order #${order.id} confirmed`,
          react: OrderConfirmationEmail({
            userName: order.userName,
            orderId: order.id,
            total: order.total,
          }),
          idempotencyKey: `order-confirmation-${order.id}`,
        });
      
        if (result.isDuplicate) {
          console.log("[Order] Confirmation already sent for:", order.id);
        }
      
        return result;
      }
      ```
      
      ---
      
      ## Pattern 4: Email Tags for Tracking
      
      Add metadata tags to emails for analytics and organization.
      
      ```typescript
      // Tags are passed directly to resend.emails.send()
      const { data, error } = await resend.emails.send({
        from,
        to,
        subject,
        html,
        tags: [
          { name: "campaign_id", value: campaign.id },
          { name: "email_type", value: "newsletter" },
          { name: "ab_variant", value: "A" },
        ],
      });
      ```
      
      **Tag constraints:**
      
      - Names and values: max 256 characters each
      - Characters: ASCII alphanumeric, underscores, dashes only (`/^[a-zA-Z0-9_-]+$/`)
      - Supported in both single send and batch API
      - Supported with scheduled emails
      
      ---
      
      ## Feature Comparison
      
      | Feature           | Use Case                          | Limit                       | Batch Support |
      | ----------------- | --------------------------------- | --------------------------- | ------------- |
      | Scheduled sending | Reminders, time-zone aware        | Up to 30 days in advance    | Not supported |
      | Idempotency keys  | Prevent duplicates, retry safety  | 256 chars, expires 24 hours | Supported     |
      | Tags              | Analytics, filtering, A/B testing | 256 chars per key/value     | Supported     |
      
      ---
      
      ## Notes
      
      - **Scheduled emails** cannot be used with batch API
      - **Attachments** cannot be used with batch API
      - **Idempotency keys** expire after 24 hours
      - **Tags** support ASCII alphanumeric characters, underscores, and dashes only
      - All features work with the standard `resend.emails.send()` method
      
    • async-batch.md 5.3 KB
      # Email - Async and Batch Sending Examples
      
      > Non-blocking email sending and batch API patterns. See [SKILL.md](../SKILL.md) for core concepts and [core.md](core.md) for basic send pattern.
      
      ---
      
      ## Pattern 1: Fire and Forget
      
      Send emails without blocking the response.
      
      ```typescript
      // lib/email/async-email.ts
      import type { SendEmailOptions } from "./send-email";
      import { sendEmailWithRetry } from "./send-with-retry";
      
      // Queue for tracking in-flight emails (for graceful shutdown)
      const inFlightEmails = new Set<Promise<unknown>>();
      
      export function sendEmailAsync(options: SendEmailOptions): void {
        const promise = sendEmailWithRetry(options)
          .catch((err) => {
            console.error("[Email] Async send failed:", err);
          })
          .finally(() => {
            inFlightEmails.delete(promise);
          });
      
        inFlightEmails.add(promise);
      }
      
      // For graceful shutdown - wait for all emails to complete
      export async function flushPendingEmails(): Promise<void> {
        await Promise.all(inFlightEmails);
      }
      ```
      
      ---
      
      ## Pattern 2: Usage in API Route
      
      Non-blocking email in a signup flow.
      
      ```typescript
      // api/signup/route.ts
      import { sendEmailAsync } from "./lib/email/async-email";
      import { WelcomeEmail } from "./templates/welcome-email";
      
      // Adapt to your web framework's route handler
      export async function handleSignup(request: Request) {
        const body = await request.json();
      
        // Create user (blocking - required for response)
        const user = await createUser(body);
      
        // Send welcome email asynchronously (non-blocking)
        sendEmailAsync({
          to: user.email,
          subject: "Welcome to Your App!",
          react: WelcomeEmail({
            userName: user.name ?? "there",
            loginUrl: `${process.env.APP_URL}/login`,
          }),
        });
      
        // Return immediately - email sends in background
        return Response.json({ success: true, user });
      }
      ```
      
      **Why good:** Signup response is fast (doesn't wait for email), email failures don't break the signup flow, flushPendingEmails enables graceful shutdown
      
      ---
      
      ## Pattern 3: Batch Email Function
      
      Send to multiple recipients efficiently using Resend's batch API.
      
      ```typescript
      // lib/email/batch-email.ts
      import { Resend } from "resend";
      import { render } from "@react-email/components";
      
      const resend = new Resend(process.env.RESEND_API_KEY);
      
      const MAX_BATCH_SIZE = 100; // Resend limit per batch request
      const DEFAULT_FROM = `${process.env.EMAIL_FROM_NAME} <${process.env.EMAIL_FROM_ADDRESS}>`;
      
      interface BatchEmailItem {
        to: string;
        subject: string;
        react: React.ReactElement;
      }
      
      interface BatchSendResult {
        success: boolean;
        data?: { id: string }[];
        errors?: string[];
      }
      
      export async function sendBatchEmails(
        emails: BatchEmailItem[],
      ): Promise<BatchSendResult> {
        const results: { id: string }[] = [];
        const errors: string[] = [];
      
        // Split into batches of MAX_BATCH_SIZE
        for (let i = 0; i < emails.length; i += MAX_BATCH_SIZE) {
          const batch = emails.slice(i, i + MAX_BATCH_SIZE);
      
          // Render all emails in batch in parallel
          const renderedEmails = await Promise.all(
            batch.map(async (email) => ({
              from: DEFAULT_FROM,
              to: email.to,
              subject: email.subject,
              html: await render(email.react),
            })),
          );
      
          try {
            const { data, error } = await resend.batch.send(renderedEmails);
      
            if (error) {
              errors.push(
                `Batch ${Math.floor(i / MAX_BATCH_SIZE) + 1}: ${error.message}`,
              );
            } else if (data) {
              results.push(...data);
            }
          } catch (err) {
            const message = err instanceof Error ? err.message : "Unknown error";
            errors.push(`Batch ${Math.floor(i / MAX_BATCH_SIZE) + 1}: ${message}`);
          }
        }
      
        return {
          success: errors.length === 0,
          data: results.length > 0 ? results : undefined,
          errors: errors.length > 0 ? errors : undefined,
        };
      }
      
      export type { BatchEmailItem, BatchSendResult };
      ```
      
      ---
      
      ## Pattern 4: Batch Usage Example
      
      Send notifications to all team members.
      
      ```typescript
      // Send notifications to all team members
      import { sendBatchEmails } from "./lib/email/batch-email";
      import { NotificationEmail } from "./templates/notification-email";
      
      async function notifyTeamMembers(
        members: { email: string; name: string }[],
        notification: { title: string; body: string; actionUrl: string },
      ) {
        const emails = members.map((member) => ({
          to: member.email,
          subject: notification.title,
          react: NotificationEmail({
            userName: member.name,
            notificationType: "update" as const,
            title: notification.title,
            body: notification.body,
            actionUrl: notification.actionUrl,
            actionText: "View Update",
            unsubscribeUrl: `${process.env.APP_URL}/unsubscribe?email=${member.email}`,
          }),
        }));
      
        const result = await sendBatchEmails(emails);
      
        if (!result.success) {
          console.error("[Email] Batch send had errors:", result.errors);
        }
      
        return result;
      }
      ```
      
      **Why good:** Handles Resend's 100-email batch limit, renders all templates in parallel, returns detailed results per batch
      
      ---
      
      ## Batch API Limitations
      
      **Not Supported in Batch:**
      
      - `attachments` field - use single send for emails with attachments
      - `scheduledAt` field - use single send for scheduled emails
      
      **Supported in Batch:**
      
      - `tags` - for analytics and tracking
      - `headers` (custom email headers)
      - `cc` and `bcc` recipients
      - Up to 50 recipients per email (`to` field)
      
    • auth-integration.md 888 B
      # Email - Authentication Integration
      
      > **This file has been removed.** Auth integration patterns belong in your authentication skill/documentation, not in the email skill.
      
      **Key guidance:** Wire your auth system's email callbacks (verification, password reset) to the `sendEmailWithRetry()` function from [core.md](core.md) Pattern 3. The email skill handles sending; your auth system handles when to trigger it.
      
      ```typescript
      // In your auth configuration, connect callbacks to the email send function:
      async function sendVerificationEmail(
        user: { email: string; name?: string },
        url: string,
      ) {
        await sendEmailWithRetry({
          to: user.email,
          subject: "Verify your email address",
          react: VerificationEmail({
            userName: user.name ?? "there",
            verificationUrl: url,
          }),
        });
      }
      ```
      
      See [core.md](core.md) for the full `sendEmailWithRetry` implementation.
      
    • core.md 7.6 KB
      # Email - Core Examples
      
      > Essential email patterns - template structure, sending, and retry. See [SKILL.md](../SKILL.md) for core concepts.
      
      **Extended Examples:**
      
      - [templates.md](templates.md) - Password Reset, Notification Templates
      - [async-batch.md](async-batch.md) - Async Sending, Batch API
      - [webhooks.md](webhooks.md) - Webhook Handler for Tracking
      - [preferences.md](preferences.md) - Unsubscribe, Email Preferences
      - [advanced-features.md](advanced-features.md) - Scheduled Sending, Idempotency Keys, Tags
      
      ---
      
      ## Pattern 1: Email Template Structure
      
      Create well-structured email templates with proper typing.
      
      ```typescript
      // templates/welcome-email.tsx
      import { Button, Heading, Link, Text } from "@react-email/components";
      
      import { BaseLayout } from "../layouts/base-layout";
      
      const CTA_PADDING_X = 24;
      const CTA_PADDING_Y = 12;
      
      // Always define props interface
      interface WelcomeEmailProps {
        userName: string;
        loginUrl: string;
        features?: string[];
      }
      
      export function WelcomeEmail({
        userName,
        loginUrl,
        features = [],
      }: WelcomeEmailProps) {
        return (
          <BaseLayout preview={`Welcome to Your App, ${userName}!`}>
            <Heading className="text-2xl font-bold text-gray-900 mb-4">
              Welcome to Your App!
            </Heading>
      
            <Text className="text-gray-600 mb-4">Hi {userName},</Text>
      
            <Text className="text-gray-600 mb-6">
              Thanks for joining! We&apos;re excited to have you on board.
            </Text>
      
            {features.length > 0 && (
              <>
                <Text className="text-gray-600 mb-2 font-semibold">
                  Here&apos;s what you can do:
                </Text>
                <ul className="text-gray-600 mb-6 pl-4">
                  {features.map((feature) => (
                    <li key={feature} className="mb-1">
                      {feature}
                    </li>
                  ))}
                </ul>
              </>
            )}
      
            <Button
              href={loginUrl}
              className="bg-blue-600 text-white font-semibold rounded-md"
              style={{
                paddingLeft: CTA_PADDING_X,
                paddingRight: CTA_PADDING_X,
                paddingTop: CTA_PADDING_Y,
                paddingBottom: CTA_PADDING_Y,
              }}
            >
              Get Started
            </Button>
          </BaseLayout>
        );
      }
      
      // Preview props for development server
      WelcomeEmail.PreviewProps = {
        userName: "John",
        loginUrl: "https://example.com/login",
        features: ["Create projects", "Invite team members", "Track progress"],
      } satisfies WelcomeEmailProps;
      
      export type { WelcomeEmailProps };
      ```
      
      **Why good:** Typed props catch errors at compile time, PreviewProps enable dev server preview, optional props have defaults, BaseLayout ensures consistency
      
      ---
      
      ## Pattern 2: Basic Send with Error Handling
      
      Send emails with proper error handling and logging.
      
      ```typescript
      // lib/email/send-email.ts
      import { Resend } from "resend";
      import { render } from "@react-email/components";
      
      const resend = new Resend(process.env.RESEND_API_KEY);
      
      const DEFAULT_FROM_NAME = "Your App";
      const DEFAULT_FROM_ADDRESS = "noreply@yourdomain.com";
      
      interface SendEmailOptions {
        to: string | string[];
        subject: string;
        react: React.ReactElement;
        replyTo?: string;
        cc?: string[];
        bcc?: string[];
      }
      
      interface SendEmailResult {
        success: boolean;
        id?: string;
        error?: string;
      }
      
      export async function sendEmail(
        options: SendEmailOptions,
      ): Promise<SendEmailResult> {
        try {
          // CRITICAL: Always await render()
          const html = await render(options.react);
      
          const { data, error } = await resend.emails.send({
            from: `${DEFAULT_FROM_NAME} <${DEFAULT_FROM_ADDRESS}>`,
            to: options.to,
            subject: options.subject,
            html,
            replyTo: options.replyTo,
            cc: options.cc,
            bcc: options.bcc,
          });
      
          if (error) {
            console.error("[Email] Send failed:", error);
            return { success: false, error: error.message };
          }
      
          console.log("[Email] Sent successfully:", data?.id);
          return { success: true, id: data?.id };
        } catch (err) {
          const message = err instanceof Error ? err.message : "Unknown error";
          console.error("[Email] Unexpected error:", message);
          return { success: false, error: message };
        }
      }
      
      export type { SendEmailOptions, SendEmailResult };
      ```
      
      **Why good:** Wraps Resend client with consistent interface, always awaits render(), returns typed result, logs for debugging
      
      ---
      
      ## Pattern 3: Retry with Exponential Backoff
      
      Implement retry logic for temporary API failures.
      
      ```typescript
      // lib/email/constants.ts
      export const MAX_RETRY_ATTEMPTS = 3;
      export const INITIAL_RETRY_DELAY_MS = 1000;
      export const RETRY_BACKOFF_MULTIPLIER = 2;
      
      // Errors that are safe to retry
      const RETRYABLE_ERRORS = [
        "rate_limit_exceeded",
        "internal_server_error",
        "service_unavailable",
      ];
      ```
      
      ```typescript
      // lib/email/send-with-retry.ts
      import { Resend } from "resend";
      import { render } from "@react-email/components";
      
      import {
        MAX_RETRY_ATTEMPTS,
        INITIAL_RETRY_DELAY_MS,
        RETRY_BACKOFF_MULTIPLIER,
      } from "./constants";
      
      interface SendWithRetryOptions {
        to: string | string[];
        subject: string;
        react: React.ReactElement;
        maxRetries?: number;
      }
      
      async function sleep(ms: number): Promise<void> {
        return new Promise((resolve) => setTimeout(resolve, ms));
      }
      
      export async function sendEmailWithRetry(
        options: SendWithRetryOptions,
      ): Promise<{ success: boolean; id?: string; error?: string }> {
        const resend = new Resend(process.env.RESEND_API_KEY);
        const maxRetries = options.maxRetries ?? MAX_RETRY_ATTEMPTS;
      
        let lastError: string | undefined;
        let attempt = 0;
      
        while (attempt < maxRetries) {
          attempt++;
      
          try {
            const html = await render(options.react);
      
            const { data, error } = await resend.emails.send({
              from: `${process.env.EMAIL_FROM_NAME} <${process.env.EMAIL_FROM_ADDRESS}>`,
              to: options.to,
              subject: options.subject,
              html,
            });
      
            if (error) {
              lastError = error.message;
      
              const isRetryable = RETRYABLE_ERRORS.some((e) =>
                error.name?.toLowerCase().includes(e),
              );
      
              if (isRetryable && attempt < maxRetries) {
                const delay =
                  INITIAL_RETRY_DELAY_MS *
                  Math.pow(RETRY_BACKOFF_MULTIPLIER, attempt - 1);
                console.log(
                  `[Email] Retry ${attempt}/${maxRetries} after ${delay}ms`,
                );
                await sleep(delay);
                continue;
              }
      
              return { success: false, error: error.message };
            }
      
            return { success: true, id: data?.id };
          } catch (err) {
            lastError = err instanceof Error ? err.message : "Unknown error";
      
            if (attempt < maxRetries) {
              const delay =
                INITIAL_RETRY_DELAY_MS *
                Math.pow(RETRY_BACKOFF_MULTIPLIER, attempt - 1);
              await sleep(delay);
              continue;
            }
          }
        }
      
        return { success: false, error: lastError ?? "Max retries exceeded" };
      }
      ```
      
      **Why good:** Exponential backoff prevents overwhelming the API, only retries transient errors, configurable retry count, logs retry attempts
      
      ---
      
      ## Anti-Pattern: Common Mistakes
      
      ```typescript
      // BAD Example - Common mistakes
      async function sendEmailBad(
        to: string,
        subject: string,
        react: React.ReactElement,
      ) {
        const resend = new Resend(process.env.RESEND_API_KEY);
      
        // BAD: Not awaiting render - sends "[object Promise]"
        const html = render(react);
      
        // BAD: No error handling - silent failures
        await resend.emails.send({
          from: "noreply@example.com", // BAD: Hardcoded
          to,
          subject,
          html, // This is a Promise, not a string!
        });
      
        // BAD: No return value - caller has no idea if it worked
      }
      ```
      
      **Why bad:** Not awaiting render() sends garbage, no error handling means silent failures, hardcoded from address breaks when domain changes
      
    • preferences.md 4.8 KB
      # Email - Preferences Examples
      
      > Email preferences and unsubscribe handling for CAN-SPAM compliance. See [SKILL.md](../SKILL.md) for core concepts and [core.md](core.md) for basic send pattern.
      
      > **Database examples below are pseudocode.** Adapt to your ORM/database solution - the patterns remain the same.
      
      ---
      
      ## Pattern 1: Preferences Schema
      
      Store user email preferences in database.
      
      ```typescript
      // lib/db/schema/email-preferences.ts
      // Adapt to your database solution
      
      // Schema concept - implement with your ORM:
      // table: email_preferences
      //   user_id: text (primary key)
      //   marketing_emails: boolean (default: true)
      //   product_updates: boolean (default: true)
      //   team_notifications: boolean (default: true)
      //   security_alerts: boolean (default: true, cannot be disabled)
      //   updated_at: timestamp
      
      interface EmailPreferences {
        userId: string;
        marketingEmails: boolean;
        productUpdates: boolean;
        teamNotifications: boolean;
        securityAlerts: boolean; // Cannot be disabled
        updatedAt: Date;
      }
      ```
      
      ---
      
      ## Pattern 2: Unsubscribe Endpoint
      
      Token-based unsubscribe for security.
      
      ```typescript
      // api/email/unsubscribe/route.ts
      import jwt from "jsonwebtoken";
      
      const UNSUBSCRIBE_SECRET = process.env.UNSUBSCRIBE_SECRET!;
      
      interface UnsubscribeToken {
        userId: string;
        category: "marketing" | "product_updates" | "team_notifications";
      }
      
      // Adapt to your web framework's route handler
      export async function handleUnsubscribe(request: Request) {
        const url = new URL(request.url);
        const token = url.searchParams.get("token");
      
        if (!token) {
          return Response.redirect("/unsubscribe-error");
        }
      
        try {
          const decoded = jwt.verify(token, UNSUBSCRIBE_SECRET) as UnsubscribeToken;
      
          // Map category to database column
          const columnMap = {
            marketing: "marketingEmails",
            product_updates: "productUpdates",
            team_notifications: "teamNotifications",
          } as const;
      
          const column = columnMap[decoded.category];
      
          // Update preference in your database
          await updateEmailPreference(decoded.userId, column, false);
      
          return Response.redirect("/unsubscribe-success");
        } catch (err) {
          return Response.redirect("/unsubscribe-error");
        }
      }
      ```
      
      ---
      
      ## Pattern 3: Generating Unsubscribe URL
      
      Create signed unsubscribe tokens.
      
      ```typescript
      // lib/email/unsubscribe.ts
      import jwt from "jsonwebtoken";
      
      const UNSUBSCRIBE_SECRET = process.env.UNSUBSCRIBE_SECRET!;
      const UNSUBSCRIBE_TOKEN_EXPIRY = "30d";
      
      type EmailCategory = "marketing" | "product_updates" | "team_notifications";
      
      export function generateUnsubscribeUrl(
        userId: string,
        category: EmailCategory,
      ): string {
        const token = jwt.sign({ userId, category }, UNSUBSCRIBE_SECRET, {
          expiresIn: UNSUBSCRIBE_TOKEN_EXPIRY,
        });
      
        return `${process.env.APP_URL}/api/email/unsubscribe?token=${token}`;
      }
      
      export type { EmailCategory };
      ```
      
      **Why good:** Token-based unsubscribe prevents unauthorized changes, security alerts cannot be disabled, preferences stored in database for checking before send
      
      ---
      
      ## Pattern 4: Checking Preferences Before Sending
      
      Respect user preferences for non-transactional emails.
      
      ```typescript
      // lib/email/send-notification.ts
      import { sendEmail } from "./send-email";
      import { generateUnsubscribeUrl } from "./unsubscribe";
      import { NotificationEmail } from "../templates/notification-email";
      
      interface NotificationOptions {
        userId: string;
        email: string;
        userName: string;
        category: "marketing" | "product_updates" | "team_notifications";
        title: string;
        body: string;
        actionUrl: string;
        actionText: string;
      }
      
      export async function sendNotificationEmail(
        options: NotificationOptions,
      ): Promise<{ sent: boolean; reason?: string }> {
        // Check user preferences (implement with your database solution)
        const preferences = await getEmailPreferences(options.userId);
      
        // Check if user has opted out of this category
        const categoryMap = {
          marketing: preferences?.marketingEmails ?? true,
          product_updates: preferences?.productUpdates ?? true,
          team_notifications: preferences?.teamNotifications ?? true,
        };
      
        if (!categoryMap[options.category]) {
          return { sent: false, reason: "User has opted out of this category" };
        }
      
        // Generate unsubscribe URL
        const unsubscribeUrl = generateUnsubscribeUrl(
          options.userId,
          options.category,
        );
      
        // Send email
        const result = await sendEmail({
          to: options.email,
          subject: options.title,
          react: NotificationEmail({
            userName: options.userName,
            notificationType: "update",
            title: options.title,
            body: options.body,
            actionUrl: options.actionUrl,
            actionText: options.actionText,
            unsubscribeUrl,
          }),
        });
      
        return { sent: result.success, reason: result.error };
      }
      ```
      
      **Why good:** Respects user preferences, includes proper unsubscribe link, returns reason if not sent
      
    • retry.md 1 KB
      # Email - Retry Logic Examples
      
      > Retry patterns for handling transient email failures. See [SKILL.md](../SKILL.md) for core concepts and [core.md](core.md) for basic send pattern.
      
      > **Note:** The complete retry implementation is in [core.md](core.md) Pattern 3. This file provides the retry strategy decision tree for quick reference.
      
      ---
      
      ## Retry Strategy Decision Tree
      
      ```
      Is the error retryable?
      +-- Rate limit --> Retry with exponential backoff
      +-- Server error (5xx) --> Retry with backoff
      +-- Invalid email --> Don't retry, log error
      +-- Authentication error --> Don't retry, check API key
      +-- Quota exceeded --> Don't retry, upgrade plan
      ```
      
      ---
      
      ## Retry Constants
      
      ```typescript
      export const MAX_RETRY_ATTEMPTS = 3;
      export const INITIAL_RETRY_DELAY_MS = 1000;
      export const RETRY_BACKOFF_MULTIPLIER = 2;
      
      // Errors that are safe to retry
      const RETRYABLE_ERRORS = [
        "rate_limit_exceeded",
        "internal_server_error",
        "service_unavailable",
      ];
      ```
      
      The full retry implementation with exponential backoff is in [core.md](core.md) Pattern 3.
      
    • templates.md 4.4 KB
      # Email - Template Examples
      
      > Template patterns for common email types. See [SKILL.md](../SKILL.md) for core concepts and [core.md](core.md) for basic template structure.
      
      ---
      
      ## Pattern 1: Password Reset Email
      
      Security-focused template with expiry messaging.
      
      ```typescript
      // templates/password-reset.tsx
      import { Button, Heading, Text } from "@react-email/components";
      
      import { BaseLayout } from "../layouts/base-layout";
      
      const CTA_PADDING_X = 24;
      const CTA_PADDING_Y = 12;
      const LINK_EXPIRY_MINUTES = 60;
      
      interface PasswordResetEmailProps {
        userName: string;
        resetUrl: string;
      }
      
      export function PasswordResetEmail({
        userName,
        resetUrl,
      }: PasswordResetEmailProps) {
        return (
          <BaseLayout preview="Reset your password">
            <Heading className="text-2xl font-bold text-gray-900 mb-4">
              Reset your password
            </Heading>
      
            <Text className="text-gray-600 mb-4">Hi {userName},</Text>
      
            <Text className="text-gray-600 mb-6">
              We received a request to reset your password. Click the button below to
              create a new password.
            </Text>
      
            <Button
              href={resetUrl}
              className="bg-blue-600 text-white font-semibold rounded-md"
              style={{
                paddingLeft: CTA_PADDING_X,
                paddingRight: CTA_PADDING_X,
                paddingTop: CTA_PADDING_Y,
                paddingBottom: CTA_PADDING_Y,
              }}
            >
              Reset Password
            </Button>
      
            <Text className="text-sm text-gray-500 mt-6">
              This link will expire in {LINK_EXPIRY_MINUTES} minutes.
            </Text>
      
            <Text className="text-sm text-gray-500 mt-4">
              If you didn&apos;t request a password reset, you can safely ignore this
              email. Your password will remain unchanged.
            </Text>
          </BaseLayout>
        );
      }
      
      PasswordResetEmail.PreviewProps = {
        userName: "John",
        resetUrl: "https://example.com/reset?token=abc123",
      } satisfies PasswordResetEmailProps;
      
      export type { PasswordResetEmailProps };
      ```
      
      **Why good:** Clear security messaging, expiry time communicated, reassurance for users who didn't request reset
      
      ---
      
      ## Pattern 2: Notification Email with Unsubscribe
      
      Template with required CAN-SPAM unsubscribe link.
      
      ```typescript
      // templates/notification-email.tsx
      import { Button, Heading, Link, Text } from "@react-email/components";
      
      import { BaseLayout } from "../layouts/base-layout";
      
      const CTA_PADDING_X = 20;
      const CTA_PADDING_Y = 10;
      
      interface NotificationEmailProps {
        userName: string;
        notificationType: "mention" | "comment" | "update";
        title: string;
        body: string;
        actionUrl: string;
        actionText: string;
        unsubscribeUrl: string;
      }
      
      const NOTIFICATION_TITLES: Record<string, string> = {
        mention: "You were mentioned",
        comment: "New comment on your post",
        update: "Project update",
      };
      
      export function NotificationEmail({
        userName,
        notificationType,
        title,
        body,
        actionUrl,
        actionText,
        unsubscribeUrl,
      }: NotificationEmailProps) {
        return (
          <BaseLayout preview={title}>
            <Text className="text-sm text-blue-600 font-medium mb-2">
              {NOTIFICATION_TITLES[notificationType]}
            </Text>
      
            <Heading className="text-xl font-bold text-gray-900 mb-4">
              {title}
            </Heading>
      
            <Text className="text-gray-600 mb-4">Hi {userName},</Text>
      
            <Text className="text-gray-600 mb-6">{body}</Text>
      
            <Button
              href={actionUrl}
              className="bg-blue-600 text-white font-semibold rounded-md"
              style={{
                paddingLeft: CTA_PADDING_X,
                paddingRight: CTA_PADDING_X,
                paddingTop: CTA_PADDING_Y,
                paddingBottom: CTA_PADDING_Y,
              }}
            >
              {actionText}
            </Button>
      
            {/* REQUIRED: Unsubscribe link for CAN-SPAM compliance */}
            <Text className="text-xs text-gray-400 mt-8 text-center">
              <Link href={unsubscribeUrl} className="text-gray-400 underline">
                Unsubscribe from these notifications
              </Link>
            </Text>
          </BaseLayout>
        );
      }
      
      NotificationEmail.PreviewProps = {
        userName: "John",
        notificationType: "mention",
        title: "Sarah mentioned you in a comment",
        body: '@John can you review this?',
        actionUrl: "https://example.com/comments/123",
        actionText: "View Comment",
        unsubscribeUrl: "https://example.com/unsubscribe?token=abc",
      } satisfies NotificationEmailProps;
      
      export type { NotificationEmailProps };
      ```
      
      **Why good:** Unsubscribe link is required for compliance, notification type enables different styling, props are fully typed
      
    • testing.md 1 KB
      # Email - Testing
      
      > **Testing guidance:** Use React Email's preview server for visual testing during development. For unit tests, `await render(YourTemplate({ props }))` returns HTML string you can assert against. Mock the Resend client in send function tests.
      
      **Preview server:**
      
      ```bash
      # Start React Email preview server for visual testing
      npx react-email dev
      # View at http://localhost:3000
      ```
      
      **Template testing pattern:**
      
      ```typescript
      // Render template and check output contains expected content
      const html = await render(
        WelcomeEmail({ userName: "John", loginUrl: "https://example.com/login" }),
      );
      
      expect(html).toContain("Welcome");
      expect(html).toContain("John");
      expect(html).toContain("https://example.com/login");
      ```
      
      **Testing checklist:**
      
      - All templates render without errors
      - Required props are validated (TypeScript)
      - Optional props have sensible defaults
      - PreviewProps are defined for dev server
      - Edge cases handled (empty arrays, null values)
      - Links are properly escaped
      - Special characters render correctly (`&apos;`, etc.)
      
    • webhooks.md 5 KB
      # Email - Webhook Examples
      
      > Webhook handler for tracking email events. See [SKILL.md](../SKILL.md) for core concepts and [core.md](core.md) for basic send pattern.
      
      ---
      
      ## Pattern 1: Webhook Handler Using Resend SDK (Recommended)
      
      Process Resend webhook events with the built-in SDK verification method.
      
      ```typescript
      // api/webhooks/resend/route.ts
      import { Resend } from "resend";
      
      const resend = new Resend(process.env.RESEND_API_KEY);
      
      interface ResendWebhookPayload {
        type:
          | "email.sent"
          | "email.delivered"
          | "email.opened"
          | "email.clicked"
          | "email.bounced"
          | "email.complained";
        data: {
          email_id: string;
          to: string[];
          subject: string;
          created_at: string;
          click?: { link: string };
        };
      }
      
      // Adapt to your web framework's request/response API
      export async function handleWebhook(request: Request) {
        try {
          // CRITICAL: Use raw text payload - JSON parsing breaks signature verification
          const payload = await request.text();
      
          // Use Resend SDK's built-in verification (recommended approach)
          // Note: header keys are short-form: id, timestamp, signature
          const event = resend.webhooks.verify({
            payload,
            headers: {
              id: request.headers.get("svix-id") ?? "",
              timestamp: request.headers.get("svix-timestamp") ?? "",
              signature: request.headers.get("svix-signature") ?? "",
            },
            webhookSecret: process.env.RESEND_WEBHOOK_SECRET!,
          }) as ResendWebhookPayload;
      
          // Store event in your database
          await saveEmailEvent({
            emailId: event.data.email_id,
            type: event.type,
            recipient: event.data.to[0],
            subject: event.data.subject,
            clickedLink: event.data.click?.link,
            occurredAt: new Date(event.data.created_at),
          });
      
          return new Response(JSON.stringify({ received: true }), { status: 200 });
        } catch (err) {
          console.error("[Webhook] Verification failed:", err);
          return new Response(
            JSON.stringify({ error: "Invalid webhook signature" }),
            { status: 400 },
          );
        }
      }
      ```
      
      **Why good:** Uses SDK's built-in verification, uses correct `webhookSecret` parameter and short-form header keys (`id`, `timestamp`, `signature`), returns 400 on invalid signatures
      
      ---
      
      ## Pattern 2: Manual Verification with Svix (Alternative)
      
      For environments where Resend SDK isn't suitable, use Svix library directly.
      
      ```typescript
      // api/webhooks/resend/route.ts
      import { Webhook } from "svix";
      
      const WEBHOOK_SECRET = process.env.RESEND_WEBHOOK_SECRET!;
      
      // Adapt to your web framework's request/response API
      export async function handleWebhook(request: Request) {
        try {
          // CRITICAL: Use raw text payload - JSON parsing breaks signature verification
          const payload = await request.text();
      
          // All three headers are required for Svix verification
          const headers = {
            "svix-id": request.headers.get("svix-id") ?? "",
            "svix-timestamp": request.headers.get("svix-timestamp") ?? "",
            "svix-signature": request.headers.get("svix-signature") ?? "",
          };
      
          // Verify using Svix library
          const wh = new Webhook(WEBHOOK_SECRET);
          const event = wh.verify(payload, headers);
      
          // Process the verified event...
      
          return new Response(JSON.stringify({ received: true }), { status: 200 });
        } catch (err) {
          console.error("[Webhook] Verification failed:", err);
          return new Response(
            JSON.stringify({ error: "Invalid webhook signature" }),
            { status: 400 },
          );
        }
      }
      ```
      
      **Why good:** Uses Svix library directly for signature verification, includes all three required headers, handles errors gracefully
      
      ---
      
      ## Pattern 3: Webhook Configuration
      
      Steps to configure webhooks in Resend Dashboard:
      
      1. Go to Resend Dashboard > Webhooks
      2. Add endpoint: `https://yourdomain.com/api/webhooks/resend`
      3. Select events: sent, delivered, opened, clicked, bounced, complained
      4. Copy the signing secret to `RESEND_WEBHOOK_SECRET`
      
      ---
      
      ## Event Types Reference
      
      | Event              | Description                     |
      | ------------------ | ------------------------------- |
      | `email.sent`       | Email accepted by Resend        |
      | `email.delivered`  | Email delivered to recipient    |
      | `email.opened`     | Recipient opened the email      |
      | `email.clicked`    | Recipient clicked a link        |
      | `email.bounced`    | Email bounced (invalid address) |
      | `email.complained` | Recipient marked as spam        |
      
      ---
      
      ## Security Notes
      
      - **Always verify webhook signatures** - prevents spoofed and replay attacks
      - **Use raw request body** (`request.text()`) - JSON parsing/stringifying breaks signature verification
      - **SDK method uses `webhookSecret`** parameter (not `secret`)
      - **SDK header keys are short-form:** `id`, `timestamp`, `signature` (mapped from `svix-id`, `svix-timestamp`, `svix-signature`)
      - **Svix method uses full header names:** `svix-id`, `svix-timestamp`, `svix-signature`
      - Store `RESEND_WEBHOOK_SECRET` in environment variables
      - Return 400 for invalid signatures, 200 for valid events
      - Prefer `resend.webhooks.verify()` SDK method over manual Svix verification
      
  • reference.md 4.1 KB
    # Email Reference
    
    > Decision frameworks, anti-patterns, and red flags for the Email skill. See [SKILL.md](SKILL.md) for core concepts and [examples/](examples/) folder for code examples.
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### Sync vs Async Sending
    
    ```
    Is email required for the response?
    ├── YES (e.g., password reset confirmation)
    │   └── Await the send, handle errors
    └── NO (e.g., welcome email, notification)
        └── Send async, don't await
    ```
    
    ### Single vs Batch Sending
    
    ```
    How many emails?
    ├── 1 email → resend.emails.send()
    ├── 2-100 emails → resend.batch.send()
    └── 100+ emails → Loop with batch API
    ```
    
    ### Retry Strategy
    
    ```
    Is the error retryable?
    ├── Rate limit → Retry with exponential backoff
    ├── Server error (5xx) → Retry with backoff
    ├── Invalid email → Don't retry, log error
    ├── Authentication error → Don't retry, check API key
    └── Quota exceeded → Don't retry, upgrade plan
    ```
    
    ### Email Category
    
    ```
    What type of email is this?
    ├── Transactional (verification, password reset)
    │   └── Always send, no unsubscribe needed
    ├── Notification (mentions, comments)
    │   └── Check preferences, include unsubscribe
    └── Marketing (promotions, newsletters)
        └── Require explicit opt-in, include unsubscribe
    ```
    
    ### Scheduled vs Immediate Sending
    
    ```
    Should email be sent now?
    ├── Time-sensitive (password reset, verification)
    │   └── Send immediately
    ├── User timezone matters (reminders, digests)
    │   └── Schedule for appropriate local time
    ├── Campaign with specific launch time
    │   └── Schedule using scheduledAt parameter
    └── Batch notification
        └── Note: scheduledAt not supported in batch API
    ```
    
    ### Idempotency Key Usage
    
    ```
    Should you use an idempotency key?
    ├── Payment/order confirmations
    │   └── YES - use order ID as key
    ├── User-triggered actions (signup, password reset)
    │   └── YES - use request ID or user+action combo
    ├── Retryable requests (webhook handlers, queues)
    │   └── YES - prevents duplicate sends on retry
    └── One-off manual sends
        └── Optional - not strictly necessary
    ```
    
    </decision_framework>
    
    ---
    
    <anti_patterns>
    
    ## Anti-Patterns
    
    ### Not Awaiting render()
    
    ```typescript
    // ANTI-PATTERN: Forgetting to await
    const html = render(WelcomeEmail({ userName }));
    await resend.emails.send({ html }); // Sends "[object Promise]"!
    ```
    
    **Why it's wrong:** render() returns a Promise, email body will be garbage.
    
    **What to do instead:** Always `const html = await render(...)`.
    
    ---
    
    ### Client-Side Email Sending
    
    ```typescript
    // ANTI-PATTERN: Exposing API key to client
    const resend = new Resend(process.env.PUBLIC_RESEND_KEY);
    // API key visible in browser bundle!
    ```
    
    **Why it's wrong:** API key exposed, anyone can send emails as you.
    
    **What to do instead:** Only send emails from server-side code.
    
    ---
    
    ### Silent Failure
    
    ```typescript
    // ANTI-PATTERN: No error handling
    await resend.emails.send({ ... });
    // If this fails, no one knows!
    ```
    
    **Why it's wrong:** Lost emails, confused users, no debugging info.
    
    **What to do instead:** Check error response, log failures, implement retry.
    
    ---
    
    ### Missing Unsubscribe
    
    ```typescript
    // ANTI-PATTERN: No unsubscribe in marketing email
    const MarketingEmail = () => (
      <BaseLayout>
        <Text>Check out our new features!</Text>
        {/* No unsubscribe link - illegal! */}
      </BaseLayout>
    );
    ```
    
    **Why it's wrong:** CAN-SPAM violation, users mark as spam instead.
    
    **What to do instead:** Always include unsubscribe link in non-transactional emails.
    
    ---
    
    ### Ignoring Preferences
    
    ```typescript
    // ANTI-PATTERN: Sending without checking preferences
    async function sendNewsletter(users: User[]) {
      for (const user of users) {
        await sendEmail({ to: user.email, ... });
        // User may have opted out!
      }
    }
    ```
    
    **Why it's wrong:** Spam to users who opted out, damages reputation.
    
    **What to do instead:** Check email preferences before sending non-transactional emails.
    
    </anti_patterns>
    
  • SKILL.md 11.1 KB
    ---
    name: api-email-resend-react-email
    description: Resend + React Email templates
    ---
    
    # Email Patterns with Resend and React Email
    
    > **Quick Guide:** Use Resend for transactional emails with React Email templates. Always `await render()` before sending (it returns a Promise). Server-side only - never expose API keys to clients. Implement retry with exponential backoff for transient failures. Include unsubscribe links in non-transactional emails (CAN-SPAM). Use `resend.batch.send()` for 2-100 recipients (no attachments or scheduling support in batch). React Email 5.0+ deprecated `renderAsync` - use `render()` instead. Webhook verification requires raw request body and `webhookSecret` parameter.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST await `render()` before passing HTML to resend.emails.send() - render returns a Promise)**
    
    **(You MUST handle Resend API errors and implement retry logic for transient failures)**
    
    **(You MUST use server-side sending for all emails - never expose RESEND_API_KEY to the client)**
    
    **(You MUST include unsubscribe links in marketing/notification emails - required for CAN-SPAM compliance)**
    
    **(You MUST use typed props interfaces for all email templates - enables compile-time validation)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** Resend, React Email, @react-email/components, resend.emails.send, email template, transactional email, verification email, password reset email, notification email, email rendering, resend.batch.send, resend.webhooks.verify
    
    **When to use:**
    
    - Sending transactional emails (verification, password reset, receipts)
    - Creating React Email templates with Tailwind styling
    - Building notification systems with email delivery
    - Implementing email tracking via webhooks
    - Batch sending to multiple recipients
    
    **When NOT to use:**
    
    - Marketing campaign management (use dedicated marketing tools)
    - SMS or push notifications (different services)
    - Email list management (use Resend Audiences or marketing tools)
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    Email in modern applications follows a **server-side, template-driven** approach. React Email brings component patterns to email development, while Resend handles reliable delivery.
    
    **Core principles:**
    
    1. **Server-side only** - Never expose API keys to clients
    2. **Typed templates** - Props interfaces catch errors at compile time
    3. **Reliable delivery** - Error handling with retry logic for transient failures
    4. **Non-blocking** - Fire-and-forget for non-critical emails
    
    **When to send emails:**
    
    - User authentication events (verification, password reset, 2FA)
    - Transactional confirmations (purchases, signups, invitations)
    - Important notifications (security alerts, account changes)
    - Team collaboration (invites, mentions, updates)
    
    **When NOT to send emails:**
    
    - Every minor action (creates email fatigue)
    - Marketing without consent (spam, illegal)
    - Real-time alerts (use push notifications)
    - In-app actions (show in-app notifications instead)
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Email Template Structure
    
    Define typed props, use React Email components, add PreviewProps for the dev server.
    
    ```typescript
    interface WelcomeEmailProps {
      userName: string;
      loginUrl: string;
      features?: string[];
    }
    
    export function WelcomeEmail({ userName, loginUrl, features = [] }: WelcomeEmailProps) {
      return (
        <BaseLayout preview={`Welcome, ${userName}!`}>
          <Heading>Welcome!</Heading>
          <Text>Hi {userName},</Text>
          <Button href={loginUrl}>Get Started</Button>
        </BaseLayout>
      );
    }
    
    WelcomeEmail.PreviewProps = { userName: "John", loginUrl: "..." } satisfies WelcomeEmailProps;
    ```
    
    See [examples/core.md](examples/core.md) Pattern 1 for complete template with layout and styling.
    
    ---
    
    ### Pattern 2: Sending with Error Handling
    
    Always await `render()`, check the `{ data, error }` response, return typed results.
    
    ```typescript
    const html = await render(options.react); // CRITICAL: must await
    
    const { data, error } = await resend.emails.send({
      from: `${DEFAULT_FROM_NAME} <${DEFAULT_FROM_ADDRESS}>`,
      to: options.to,
      subject: options.subject,
      html,
    });
    
    if (error) {
      return { success: false, error: error.message };
    }
    return { success: true, id: data?.id };
    ```
    
    See [examples/core.md](examples/core.md) Pattern 2-3 for complete send wrapper and retry with exponential backoff.
    
    ---
    
    ### Pattern 3: Async (Fire-and-Forget) Sending
    
    For non-critical emails (welcome, notifications), don't block the response.
    
    ```typescript
    // Non-blocking - catches errors internally
    sendEmailAsync(options);
    return Response.json({ success: true }); // Returns immediately
    ```
    
    Track in-flight promises for graceful shutdown. See [examples/async-batch.md](examples/async-batch.md) Pattern 1-2.
    
    ---
    
    ### Pattern 4: Batch API
    
    Use `resend.batch.send()` for 2-100 recipients. Render all templates in parallel.
    
    ```typescript
    const rendered = await Promise.all(
      emails.map(async (e) => ({
        from,
        to: e.to,
        subject: e.subject,
        html: await render(e.react),
      })),
    );
    const { data, error } = await resend.batch.send(rendered);
    ```
    
    **Batch limitations:** No `attachments`, no `scheduledAt`. Tags and idempotency keys are supported. See [examples/async-batch.md](examples/async-batch.md) Pattern 3-4.
    
    ---
    
    ### Pattern 5: Webhook Verification
    
    Use `resend.webhooks.verify()` with the raw request body. JSON parsing breaks signature verification.
    
    ```typescript
    const payload = await request.text(); // Raw body, NOT .json()
    
    const event = resend.webhooks.verify({
      payload,
      headers: {
        id: request.headers.get("svix-id") ?? "",
        timestamp: request.headers.get("svix-timestamp") ?? "",
        signature: request.headers.get("svix-signature") ?? "",
      },
      webhookSecret: process.env.RESEND_WEBHOOK_SECRET!,
    });
    ```
    
    See [examples/webhooks.md](examples/webhooks.md) for full handler with event processing and Svix alternative.
    
    ---
    
    ### Pattern 6: Scheduled Sending, Idempotency Keys, Tags
    
    ```typescript
    // Scheduled (up to 30 days, NOT supported in batch)
    await resend.emails.send({ ...payload, scheduledAt: futureDate.toISOString() });
    
    // Idempotency (256 char limit, expires 24h) — second argument to send()
    await resend.emails.send(payload, {
      idempotencyKey: `order-confirmation-${orderId}`,
    });
    
    // Tags (ASCII alphanumeric, underscores, dashes only)
    await resend.emails.send({
      ...payload,
      tags: [{ name: "campaign", value: "launch" }],
    });
    ```
    
    See [examples/advanced-features.md](examples/advanced-features.md) for complete implementations with validation.
    
    ---
    
    ### Pattern 7: Unsubscribe and Preferences
    
    Non-transactional emails MUST include unsubscribe links (CAN-SPAM). Use signed tokens for security.
    
    ```typescript
    // In every notification/marketing template:
    <Link href={unsubscribeUrl}>Unsubscribe from these notifications</Link>
    
    // Generate signed unsubscribe URLs
    const token = jwt.sign({ userId, category }, UNSUBSCRIBE_SECRET, { expiresIn: "30d" });
    const url = `${APP_URL}/api/email/unsubscribe?token=${token}`;
    ```
    
    See [examples/preferences.md](examples/preferences.md) for preference schema, checking before send, and unsubscribe endpoint.
    
    </patterns>
    
    ---
    
    **Detailed Resources:**
    
    - [examples/core.md](examples/core.md) - Template structure, sending with error handling, retry logic
    - [examples/async-batch.md](examples/async-batch.md) - Async sending, batch API
    - [examples/webhooks.md](examples/webhooks.md) - Webhook handler with signature verification
    - [examples/templates.md](examples/templates.md) - Password Reset, Notification templates
    - [examples/preferences.md](examples/preferences.md) - Unsubscribe, email preferences
    - [examples/advanced-features.md](examples/advanced-features.md) - Scheduled sending, idempotency keys, tags
    - [reference.md](reference.md) - Decision frameworks, anti-patterns
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - Not awaiting `render()` - sends `"[object Promise]"` as email body
    - API key exposed on client - security vulnerability
    - No error handling - silent failures
    - Missing unsubscribe links in non-transactional emails - CAN-SPAM violation
    - Sending without checking user preferences - spam
    
    **Medium Priority Issues:**
    
    - No retry logic for transient failures (rate limits, 5xx errors)
    - Sync sending blocking request handlers for non-critical emails
    - Hardcoded from address instead of environment variable
    - No webhook verification signature check
    - Not logging email send results
    
    **Common Mistakes:**
    
    - Using `Grid` or `Flexbox` in email templates (not supported by email clients)
    - Expecting shadows or gradients to render in emails
    - Using `rem` units (email clients handle differently)
    - Forgetting `PreviewProps` for dev server
    
    **Gotchas & Edge Cases:**
    
    - Resend SDK accepts a `react` prop directly (renders internally), but pre-rendering with `await render()` + `html` gives you control over the output and works outside the Resend SDK
    - `render()` is async in React Email 5.0+ (`renderAsync` deprecated)
    - Batch API limited to 100 emails, does NOT support `attachments` or `scheduledAt`
    - Webhooks require raw request body - JSON parsing breaks signature verification
    - Webhook verify uses `webhookSecret` parameter (not `secret`)
    - Webhook headers object uses short keys: `id`, `timestamp`, `signature`
    - Idempotency keys are passed as a second argument to `resend.emails.send()`, not in the email payload headers
    - Idempotency keys expire after 24 hours, max 256 characters
    - Tags: ASCII alphanumeric, underscores, dashes only, max 256 chars per key/value
    - Tailwind in emails requires `@react-email/tailwind` wrapper (Tailwind 4 supported in React Email 5.0+)
    - Images must use absolute URLs (no relative paths)
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST await `render()` before passing HTML to resend.emails.send() - render returns a Promise)**
    
    **(You MUST handle Resend API errors and implement retry logic for transient failures)**
    
    **(You MUST use server-side sending for all emails - never expose RESEND_API_KEY to the client)**
    
    **(You MUST include unsubscribe links in marketing/notification emails - required for CAN-SPAM compliance)**
    
    **(You MUST use typed props interfaces for all email templates - enables compile-time validation)**
    
    **Failure to follow these rules will cause email delivery failures, security vulnerabilities, or legal compliance issues.**
    
    </critical_reminders>
    
    ---
    
    ## Sources
    
    - [Resend Node.js SDK](https://resend.com/docs/send-with-nodejs)
    - [Resend Send Email API](https://resend.com/docs/api-reference/emails/send-email)
    - [Resend Batch API](https://resend.com/docs/api-reference/emails/send-batch-emails)
    - [Resend Webhooks Verification](https://resend.com/docs/dashboard/webhooks/verify-webhooks-requests)
    - [Resend Idempotency Keys](https://resend.com/blog/engineering-idempotency-keys)
    - [Resend Error Handling](https://resend.com/docs/api-reference/errors)
    - [React Email Components](https://react.email/docs/components)
    - [React Email 5.0 Release](https://resend.com/blog/react-email-5)
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related