| name | resend-email-integration |
| description | Implement or integrate Resend transactional emails in any Node.js project. Complete API reference (21 topics), error handling (26 codes), 20 documented gaps, production-ready templates, and working examples. Framework-agnostic (Next.js, Express, Fastify). Use when sending emails, implementing webhooks, handling bounces, monitoring deliverability, or troubleshooting email issues. |
| license | MIT |
| compatibility | Requires Node.js 18+, TypeScript 4.5+, and the Resend SDK (`npm install resend`). Works with any framework (Next.js, Express, Fastify, etc.). Requires RESEND_API_KEY environment variable. |
| metadata | {"version":"1.0.0","provider":"resend","body-language":"en","reference-language":"pt-BR","source":"extracted from production Resend API documentation + implementation patterns"} |
Resend Email Integration
A complete, production-ready guide to transactional email via Resend: 21 API topics, 26 error codes, 20 documented gaps, 4 templates, 3 working examples, plus implementation patterns for any Node.js framework.
Reference docs are in Brazilian Portuguese (references/), but this skill is framework and language agnostic. Code examples are TypeScript/JavaScript. Translate templates and copy for your product.
0. Orient yourself before touching anything
Three questions decide what you do next:
-
Is Resend already integrated here?
grep -r "RESEND_API_KEY" .env*
grep -r "from 'resend'" src/
grep -r "resend.emails.send" src/
- Nothing → this is a fresh implementation: start with the templates below.
- Partially → find what's missing (webhooks? error handling? retry?).
- Fully → you are maintaining or adding features. Read the relevant reference section.
-
Something is broken? → Resend failures are usually one of 5 known issues:
- Rate limited (429) → implement exponential backoff
- Domain not verified → verify SPF/DKIM in Resend dashboard
- Webhook not processing → check HMAC verification (use SDK, not manual)
- Duplicate emails → add idempotency key
- High bounce rate → monitor and add to suppressions automatically
-
Just answering a question? → the contract below usually suffices; open a reference only when you need the detail.
1. Architecture in one picture
YOUR APPLICATION (Node.js)
Email trigger (user signup, order placed, status changed)
Call: emailService.send({ to, subject, html })
│ idempotencyKey for dedup
│ retry with exponential backoff
▼
RESEND API
────────────────────────────────────────────────────────
POST /emails ← single email send
POST /emails/batch ← up to 100 emails per request
GET /emails/{id} ← check status
GET /logs ← retrieve history
│
▼ (async)
🌐 Email Service (SMTP delivery to recipient)
│
├─ Sent (email in queue)
├─ Delivered (received by ISP)
├─ Bounced (invalid address)
├─ Complained (marked as spam)
└─ Failed (error during send)
│
────────────────────────────────────────────────────────
POST /webhooks ← register webhook endpoint
Webhook Events ──────→ YOUR /api/webhooks/resend
(email.sent, email.delivered,
email.bounced, email.complained)
│ HMAC verification (use SDK)
│ update database, add to suppressions
▼
YOUR DATABASE
EmailLog (status: sent/delivered/bounced/complained)
Suppressions (auto-added hard bounces)
The end-to-end flow, in five steps:
- Application triggers email send:
emailService.send({ to, subject, html, idempotencyKey })
- Service makes
POST /emails with idempotency key (prevents duplicates on retry)
- Resend responds with email ID or error → service retries 5xx, fails on 4xx
- Email travels through ISP delivery, generates webhook events
- Your webhook handler (
POST /api/webhooks/resend) receives events, verifies HMAC (via SDK), updates database and suppressions
2. Resend contract — the part you must get right
Base URL: https://api.resend.com
Content-Type: application/json
Authentication: Authorization: Bearer re_xxx (your API key from RESEND_API_KEY env var)
Send Email (Single)
POST /emails
Authorization: Bearer re_xxx
Idempotency-Key: "user-signup-123"
{
"from": "notify@verified-domain.com",
"to": ["user@example.com"],
"cc": ["manager@example.com"],
"bcc": ["archive@example.com"],
"subject": "Welcome to our app",
"html": "<h1>Hello!</h1>",
"text": "Hello!",
"reply_to": "support@example.com",
"tags": [
{ "name": "category", "value": "signup" }
],
"attachments": [
{
"filename": "invoice.pdf",
"content": "base64encodedcontent"
}
]
}
{
"id": "email_123abc...",
: ,
: [],
:
}
rate_limit_exceeded → backoff, retry
validation_error → fix and retry
missing_api_key → check
restricted_api_key → use full access key
invalid_idempotent_request → same key, different payload
internal_server_error → retry
Send Batch (Up to 100)
POST /emails/batch
[
{
"from": "notify@verified-domain.com",
"to": "user1@example.com",
"subject": "Message for user 1",
"html": "<p>...</p>"
},
{
"from": "notify@verified-domain.com",
"to": "user2@example.com",
"subject": "Message for user 2",
"html": "<p>...</p>"
}
]
[
{ "id": "email_123...", "created_at": "..." },
{ "id": "email_456...", "created_at": "..." }
]
Webhook Events
svix-id: "msg_..."
svix-timestamp: "1234567890"
svix-signature: "v1,..."
email.sent → Email accepted by Resend
email.delivered → Delivered to recipient ISP
email.bounced → Recipient address invalid (hard/soft)
email.complained → Recipient marked as spam
email.suppressed → Email suppressed (already bounced)
{
"type": "email.delivered",
"created_at": "2026-08-26T10:31:00Z",
"data": {
"id": "email_123...",
"from": "notify@verified-domain.com",
"to": "user@example.com",
"subject": "Welcome",
"created_at": "2026-08-26T10:30:00Z"
}
}
3. Core patterns you must know
Pattern 1: Send with Idempotency
import { emailService } from '@/lib/email';
const result = await emailService.send({
to: 'user@example.com',
subject: 'Password Reset',
html: '<a href="...">Reset</a>',
idempotencyKey: `password-reset-${userId}`,
});
if (result.success) {
return { emailId: result.id };
} else if (result.error?.isRetryable) {
} else {
}
Pattern 2: Handle All 26 Error Codes
const errors = {
'429': { name: 'rate_limit_exceeded', retry: true },
'500': { name: 'internal_server_error', retry: true },
'503': { name: 'service_unavailable', retry: true },
'400': { name: 'validation_error', retry: false },
'401': { name: 'missing_api_key', retry: false },
'403': { name: 'restricted_api_key', retry: false },
'409': { name: 'invalid_idempotent_request', retry: false },
'429-quota': { name: 'daily_quota_exceeded', retry: false },
};
Pattern 3: Retry with Exponential Backoff
async function sendWithRetry(payload, attempt = 0) {
const { data, error } = await resend.emails.send(payload);
if (!error) return data;
if (!error.isRetryable || attempt >= 3) throw error;
const delayMs = Math.pow(2, attempt) * 1000;
await new Promise(r => setTimeout(r, delayMs));
return sendWithRetry(payload, attempt + 1);
}
Pattern 4: Webhook Verification (HMAC)
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function handleWebhook(req) {
const rawBody = await req.text();
try {
const event = resend.webhooks.verify({
payload: rawBody,
headers: {
id: req.headers.get('svix-id'),
timestamp: req.headers.get('svix-timestamp'),
signature: req.headers.get('svix-signature'),
},
webhookSecret: process.env.RESEND_WEBHOOK_SECRET,
});
return new Response(JSON.stringify({ received: true }), { status: });
} (err) {
(.({ : }), { : });
}
}
Pattern 5: Add Bounced Emails to Suppressions
async function handleBounce(event) {
const emailAddress = event.data.to[0];
await resend.contacts.create({
email: emailAddress,
unsubscribed: true,
});
await db.emailLog.update({
where: { emailId: event.data.id },
data: { status: 'bounced' },
});
}
4. Implementation checklist
Phase 1: Setup (1 hour)
Phase 2: Sending (2-3 hours)
Phase 3: Webhooks (2-3 hours)
Phase 4: Monitoring (1-2 hours)
5. Common mistakes (don't make these!)
| Mistake | Why It Fails | Solution |
|---|
| try/catch for API errors | Resend SDK returns { error }, doesn't throw | Check error object, only try/catch for network errors |
| Hardcode API key | Exposed in code, git history, logs | Use .env variables, never log |
| Manual HMAC verification | Easy to get signing algorithm wrong | Use resend.webhooks.verify() from SDK |
| Parse JSON before webhook verify | Can't verify signature on parsed JSON | Capture raw body first, then verify |
| No idempotency key | Duplicate emails on network retry | Always add idempotencyKey: <unique-key> |
| Retry on 4xx errors | 4xx = validation/auth, won't fix with retry | Check error.isRetryable, only retry 5xx/429 |
| Ignore bounce rate | Resend degrades reputation if > 4% | Monitor and add bounced emails to suppressions |
| Schedule future emails in v1 | Not v1 scope, causes confusion | Implement batching/queuing instead |
6. Reference docs location
All detailed documentation lives in docs/:
| File | Use When |
|---|
README.md | First-time overview (15 min) |
GETTING-STARTED.md | Quick start (30 min) |
QUICK-REFERENCE.md | Cheat sheet (keep open while coding) |
docs/base/00-INDICE.md | Need overview of all 21 topics |
docs/base/00-LACUNAS.md | Something is undocumented/broken |
docs/base/02-send-email.md | Details on single send endpoint |
docs/base/03-send-batch-emails.md | Details on batch sending |
docs/base/05-errors.md | Deep dive on 26 error codes |
docs/base/06-rate-limits.md | Rate limit details (10 req/s) |
docs/base/08-idempotency-keys.md | Idempotency TTL and key format |
docs/base/12-webhooks-introduction.md | Webhook setup and configuration |
docs/base/14-verify-webhooks-requests.md | HMAC verification deep dive |
docs/base/16-email-bounces.md | Bounce types and handling |
reference/error-codes.json | Lookup error code solutions |
7. Templates (copy into your project)
Copy EmailService
cp templates/email-service.template.ts src/lib/email.ts
Includes:
- Type-safe send method
- Built-in retry logic (3 attempts, backoff)
- Error parsing (26 codes)
- Idempotency support
- No framework dependencies
Copy Webhook Handler
cp templates/webhook-handler.template.ts src/app/api/webhooks/resend/route.ts
Includes:
- HMAC verification via SDK
- Raw body capture
- Event routing
- Async processing (returns 200 immediately)
8. Code examples
Example 1: Simple Send
See examples/01-simple-send.ts — basic email with error checking.
Example 2: Batch Send
See examples/02-batch-send.ts — 100 emails per request with error tracking.
Example 3: Webhook Handler
See examples/03-webhook-handler.ts — HMAC verification and event processing.
9. Critical numbers (memorize!)
| Number | Why | Where |
|---|
| 10 req/s | Rate limit per team | Implement backoff at 429 |
| < 4% | Bounce rate threshold | Above = reputation loss |
| 100 | Batch email max | Split larger batches |
| 50 | Recipients per email | Split if more |
| 40 MB | Attachments total | Per email |
| 75 | Tags per email | Max count |
| 24h | Idempotency TTL | Key expires after 24h |
| 30 | Schedule days max | Maximum future schedule |
| 8x | Webhook retries | Over ~97 hours |
10. If something goes wrong
- Error code? → Search
reference/error-codes.json
- Undocumented behavior? → Check
docs/base/00-LACUNAS.md (20 known gaps)
- Webhook not working? → Read
docs/base/14-verify-webhooks-requests.md (HMAC is the #1 issue)
- High bounce rate? → Read
docs/base/16-email-bounces.md (add to suppressions!)
- Still stuck? → Open an issue on GitHub with error logs (never commit credentials)
11. Framework-specific guidance
This integration is framework-agnostic. Patterns shown above work in:
- ✅ Next.js (App Router or Pages)
- ✅ Express
- ✅ Fastify
- ✅ Any Node.js 18+ app
For framework-specific examples, see implementation-guide/ (structure in place).
Legal & Support
- License: MIT (see LICENSE file)
- Source: Based on official Resend API docs
- Bugs/Questions: Open issue on GitHub
- Resend Docs: https://resend.com/docs