---
name: resend_email_integration
description: Send transactional email through the Resend API (send, batch, scheduled, attachments, React Email), verify delivery webhooks with Svix signatures, and set up SPF/DKIM/DMARC for real deliverability. Triggers on "send email via Resend", "set up transactional email", "Resend webhook bounces", "email deliverability setup", Resend API integration.
---

# Resend Email Integration

## Purpose
Wire up transactional email in an app using Resend's API: single sends, batches, scheduled sends, attachments, React Email templates, bounce/complaint webhooks, and domain authentication. Covers the full loop from API key to delivered inbox, including the failure paths that matter in production.

## Workflow

1. **Prereqs.** Install the SDK (`npm install resend` or `pip install resend`), set `RESEND_API_KEY` (starts with `re_`). For anything beyond testing, verify a sending domain first (see references/deliverability.md). The shared `onboarding@resend.dev` sandbox only sends to the email the account registered with, so a verified domain is a hard requirement for real recipients.

2. **Send.** Use the smallest variant that fits. Examples are directly executable.

   Single (Node, the { data, error } pattern is mandatory):
   ```ts
   import { Resend } from 'resend';
   const resend = new Resend(process.env.RESEND_API_KEY);

   const { data, error } = await resend.emails.send({
     from: 'Acme <hello@yourdomain.com>',
     to: ['user@example.com'],
     subject: 'Welcome',
     html: '<p>Thanks for signing up.</p>',
     text: 'Thanks for signing up.',  // always include a text part
   });
   if (error) { console.error('Send failed:', error.message); return; }
   console.log('Sent:', data.id);
   ```

   Python with idempotency (prevents duplicate sends on retry):
   ```python
   import os, resend
   resend.api_key = os.environ["RESEND_API_KEY"]
   resend.Emails.send(
       {"from": "Acme <hello@yourdomain.com>",
        "to": ["user@example.com"],
        "subject": "Welcome",
        "html": "<p>Thanks for signing up.</p>"},
       idempotency_key=f"welcome-email/{user_id}",
   )
   ```

   Batch (2-100 distinct emails, one call):
   ```ts
   await resend.batch.send([
     { from: 'Acme <hello@yourdomain.com>', to: 'a@x.com', subject: 'Digest', html: '<p>A</p>' },
     { from: 'Acme <hello@yourdomain.com>', to: 'b@x.com', subject: 'Digest', html: '<p>B</p>' },
   ]);
   ```

   Scheduled (single sends only, not batch):
   ```ts
   await resend.emails.send({
     from: 'Acme <hello@yourdomain.com>',
     to: ['user@example.com'],
     subject: 'Reminder',
     html: '<p>Tomorrow at 9am.</p>',
     scheduledAt: 'tomorrow at 9am',  // natural language or ISO date like 2026-11-01T09:00:00Z
   });
   ```

   Attachment (base64 content):
   ```ts
   await resend.emails.send({
     from: 'Acme <hello@yourdomain.com>',
     to: ['user@example.com'],
     subject: 'Your invoice',
     html: '<p>Attached.</p>',
     attachments: [{ filename: 'invoice.pdf', content: pdfBuffer.toString('base64') }],
   });
   ```

   React Email template (rendered component):
   ```ts
   import { WelcomeEmail } from '@/emails/welcome';
   await resend.emails.send({
     from: 'Acme <hello@yourdomain.com>',
     to: ['user@example.com'],
     subject: 'Welcome',
     react: WelcomeEmail({ name: 'Alice' }),
   });
   ```

3. **Choose single vs batch correctly.** Single (`POST /emails`): one email, attachments, scheduling. Batch (`POST /emails/batch`): 2-100 emails, atomic (one invalid email fails the whole batch), no attachments, no scheduling.

4. **Handle errors.** 400/422 means the request is wrong, do not retry. 401 means check the key or its scope. 403 means the sending domain is not verified. 409 means an idempotency conflict. 429 means back off. 5xx means retry with exponential backoff. Idempotency keys expire after 24 hours and cap at 256 characters.

5. **Receive delivery events.** Register a webhook in the Resend dashboard, store the signing secret (starts with `whsec_`) as `RESEND_WEBHOOK_SECRET`, and verify every inbound event before trusting it. Respond 200 on success so Resend stops retrying. Full pattern in references/webhooks.md.

6. **Prove deliverability.** Verify the domain with SPF/DKIM/DMARC before sending to real users, then test with mail-tester.com. Steps in references/deliverability.md.

## Output Contract
- A working send path: verified domain, send function with the { data, error } check, idempotency keys on retried paths, error mapping per code above.
- A verified webhook endpoint that records bounces/complaints and stops delivery retries by returning 200.
- Evidence of deliverability: domain shows verified in the dashboard, mail-tester score checked after DNS changes.

## Operating Rules
1. Never send production email from `onboarding@resend.dev`. Verify a domain first; the sandbox errors on any other recipient.
2. Check `error` on every send. The Node SDK does not throw on API errors.
3. Verify webhook signatures on the raw request body, never on parsed JSON. An unverified webhook is untrusted input.
4. Use idempotency keys (`<event-type>/<entity-id>`) on every retried or queue-driven send. Same key plus different payload returns 409.
5. Batch sends are atomic with no attachments and no scheduling. If one send needs attachments or a future time, it goes single.
6. `email.received` webhooks carry metadata only. Fetch the body with `resend.emails.receiving.get(email_id)` when needed.
7. Hard bounces and spam complaints auto-suppress the address in Resend. Respect the suppression list, do not re-add those addresses manually.
