git clone https://github.com/agents-inc/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/agents-inc/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/src/skills/api-email-resend-react-email" ~/.claude/skills/agents-inc-skills-api-email-resend-react-email-ca67e9 && rm -rf "$T"
src/skills/api-email-resend-react-email/SKILL.mdEmail Patterns with Resend and React Email
Quick Guide: Use Resend for transactional emails with React Email templates. Always
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). Useawait render()for 2-100 recipients (no attachments or scheduling support in batch). React Email 5.0+ deprecatedresend.batch.send()- userenderAsyncinstead. Webhook verification requires raw request body andrender()parameter.webhookSecret
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)import type
(You MUST await
before passing HTML to resend.emails.send() - render returns a Promise)render()
(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:
- Server-side only - Never expose API keys to clients
- Typed templates - Props interfaces catch errors at compile time
- Reliable delivery - Error handling with retry logic for transient failures
- 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)
<patterns>
Core Patterns
Pattern 1: Email Template Structure
Define typed props, use React Email components, add PreviewProps for the dev server.
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 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.
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 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.
// 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 Pattern 1-2.
Pattern 4: Batch API
Use
resend.batch.send() for 2-100 recipients. Render all templates in parallel.
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 Pattern 3-4.
Pattern 5: Webhook Verification
Use
resend.webhooks.verify() with the raw request body. JSON parsing breaks signature verification.
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 for full handler with event processing and Svix alternative.
Pattern 6: Scheduled Sending, Idempotency Keys, Tags
// 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 for complete implementations with validation.
Pattern 7: Unsubscribe and Preferences
Non-transactional emails MUST include unsubscribe links (CAN-SPAM). Use signed tokens for security.
// 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 for preference schema, checking before send, and unsubscribe endpoint.
</patterns>Detailed Resources:
- examples/core.md - Template structure, sending with error handling, retry logic
- examples/async-batch.md - Async sending, batch API
- examples/webhooks.md - Webhook handler with signature verification
- examples/templates.md - Password Reset, Notification templates
- examples/preferences.md - Unsubscribe, email preferences
- examples/advanced-features.md - Scheduled sending, idempotency keys, tags
- reference.md - Decision frameworks, anti-patterns
<red_flags>
RED FLAGS
High Priority Issues:
- Not awaiting
- sendsrender()
as email body"[object Promise]" - 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
orGrid
in email templates (not supported by email clients)Flexbox - Expecting shadows or gradients to render in emails
- Using
units (email clients handle differently)rem - Forgetting
for dev serverPreviewProps
Gotchas & Edge Cases:
- Resend SDK accepts a
prop directly (renders internally), but pre-rendering withreact
+await render()
gives you control over the output and works outside the Resend SDKhtml
is async in React Email 5.0+ (render()
deprecated)renderAsync- Batch API limited to 100 emails, does NOT support
orattachmentsscheduledAt - Webhooks require raw request body - JSON parsing breaks signature verification
- Webhook verify uses
parameter (notwebhookSecret
)secret - Webhook headers object uses short keys:
,id
,timestampsignature - Idempotency keys are passed as a second argument to
, not in the email payload headersresend.emails.send() - 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
wrapper (Tailwind 4 supported in React Email 5.0+)@react-email/tailwind - 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,
, named constants)import type
(You MUST await
before passing HTML to resend.emails.send() - render returns a Promise)render()
(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>