Resend API integration patterns, authentication, error handling, and rate limiting. Use when implementing API clients, handling authentication, managing rate limits, implementing retry strategies, or building resilient email service integrations.
Limited to specific tools
Additional assets for this skill
This skill is limited to using the following tools:
examples/error-handling/README.mdexamples/python-client/README.mdexamples/typescript-client/README.mdComprehensive patterns and best practices for integrating with the Resend API, covering authentication, rate limiting, error handling, and resilient retry strategies.
Bearer token authentication for all Resend API requests:
import { Resend } from 'resend';
// Initialize with API key from environment
const resend = new Resend(process.env.RESEND_API_KEY);
// Or with explicit initialization
const apiKey = process.env.RESEND_API_KEY;
if (!apiKey) {
throw new Error('RESEND_API_KEY environment variable is not set');
}
const resend = new Resend(apiKey);
// For custom HTTP requests (if needed)
async function makeAuthenticatedRequest(endpoint: string, body: any) {
const response = await fetch(`https://api.resend.com/${endpoint}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
return response.json();
}
import os
from resend import Resend
# Initialize with API key from environment
api_key = os.environ.get("RESEND_API_KEY")
if not api_key:
raise ValueError("RESEND_API_KEY environment variable is not set")
client = Resend(api_key=api_key)
# For custom HTTP requests (if needed)
import httpx
async def make_authenticated_request(endpoint: str, body: dict):
async with httpx.AsyncClient() as client:
response = await client.post(
f"https://api.resend.com/{endpoint}",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json=body,
)
return response.json()
Resend API rate limits: 2 requests per second per account
class ResendAPIClient {
private requestQueue: Array<() => Promise<any>> = [];
private isProcessing = false;
private readonly requestsPerSecond = 2;
private lastRequestTime = 0;
async executeWithRateLimit<T>(fn: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.requestQueue.push(async () => {
try {
const result = await fn();
resolve(result);
} catch (error) {
reject(error);
}
});
this.processQueue();
});
}
private async processQueue() {
if (this.isProcessing || this.requestQueue.length === 0) {
return;
}
this.isProcessing = true;
while (this.requestQueue.length > 0) {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
const delayNeeded = (1000 / this.requestsPerSecond) - timeSinceLastRequest;
if (delayNeeded > 0) {
await new Promise(resolve => setTimeout(resolve, delayNeeded));
}
const request = this.requestQueue.shift();
if (request) {
await request();
this.lastRequestTime = Date.now();
}
}
this.isProcessing = false;
}
async sendEmail(payload: any) {
return this.executeWithRateLimit(() =>
resend.emails.send(payload)
);
}
async sendBatch(emails: any[]) {
return this.executeWithRateLimit(() =>
resend.batch.send(emails)
);
}
}
import asyncio
import time
from typing import TypeVar, Callable, Coroutine, Any
T = TypeVar('T')
class ResendAPIClient:
def __init__(self, api_key: str, requests_per_second: int = 2):
self.client = Resend(api_key=api_key)
self.request_queue = asyncio.Queue()
self.is_processing = False
self.requests_per_second = requests_per_second
self.last_request_time = 0
async def execute_with_rate_limit(
self,
fn: Callable[[], Coroutine[Any, Any, T]]
) -> T:
await self.request_queue.put(fn)
asyncio.create_task(self.process_queue())
# Wait for result (simplified - in production use proper queuing)
return await fn()
async def process_queue(self):
if self.is_processing or self.request_queue.empty():
return
self.is_processing = True
while not self.request_queue.empty():
now = time.time()
time_since_last = now - self.last_request_time
delay_needed = (1.0 / self.requests_per_second) - time_since_last
if delay_needed > 0:
await asyncio.sleep(delay_needed)
try:
fn = self.request_queue.get_nowait()
await fn()
self.last_request_time = time.time()
except asyncio.QueueEmpty:
break
self.is_processing = False
async def send_email(self, payload: dict):
async def send():
return self.client.emails.send(payload)
return await self.execute_with_rate_limit(send)
HTTP status codes and error handling:
| Code | Error | Handling Strategy |
|---|---|---|
| 200 | Success | Process response normally |
| 400 | Bad Request | Validate request payload, check required fields |
| 401 | Unauthorized | Verify API key is correct and valid |
| 403 | Forbidden | Check API key has required permissions |
| 404 | Not Found | Verify resource ID/email address exists |
| 409 | Conflict | Handle duplicate resource creation attempts |
| 429 | Rate Limited | Implement exponential backoff retry |
| 500 | Server Error | Retry with exponential backoff |
| 502 | Bad Gateway | Retry with exponential backoff |
| 503 | Service Unavailable | Retry with exponential backoff |
interface APIError {
code: number;
message: string;
statusText: string;
}
async function handleAPIError(error: any): Promise<void> {
if (error.response) {
const status = error.response.status;
const data = error.response.data;
switch (status) {
case 400:
console.error('Bad Request:', data.message);
throw new Error(`Invalid request: ${data.message}`);
case 401:
console.error('Unauthorized: Check API key');
throw new Error('Invalid API key. Set RESEND_API_KEY environment variable.');
case 403:
console.error('Forbidden: Insufficient permissions');
throw new Error('API key lacks required permissions.');
case 404:
console.error('Not Found:', data.message);
throw new Error(`Resource not found: ${data.message}`);
case 409:
console.error('Conflict: Resource already exists');
throw new Error(`Duplicate resource: ${data.message}`);
case 429:
console.warn('Rate limited: Implement retry');
throw new Error('Rate limit exceeded. Retry after delay.');
case 500:
case 502:
case 503:
console.error(`Server error (${status}): Retry recommended`);
throw new Error(`Server error (${status}). Retry in progress...`);
default:
console.error(`Unknown error (${status}):`, data);
throw new Error(`API error: ${data.message || 'Unknown error'}`);
}
}
throw error;
}
import logging
from typing import Optional, Dict, Any
logger = logging.getLogger(__name__)
class ResendAPIError(Exception):
"""Base exception for Resend API errors"""
pass
class AuthenticationError(ResendAPIError):
"""API authentication failed"""
pass
class RateLimitError(ResendAPIError):
"""Rate limit exceeded"""
pass
class ServerError(ResendAPIError):
"""Server-side error"""
pass
def handle_api_error(error: Exception, status_code: Optional[int] = None) -> None:
"""Handle Resend API errors based on status code"""
if status_code == 400:
logger.error(f"Bad Request: {str(error)}")
raise ResendAPIError(f"Invalid request: {str(error)}")
elif status_code == 401:
logger.error("Unauthorized: Check API key")
raise AuthenticationError(
"Invalid API key. Set RESEND_API_KEY environment variable."
)
elif status_code == 403:
logger.error("Forbidden: Insufficient permissions")
raise AuthenticationError("API key lacks required permissions.")
elif status_code == 404:
logger.error(f"Not Found: {str(error)}")
raise ResendAPIError(f"Resource not found: {str(error)}")
elif status_code == 409:
logger.error("Conflict: Resource already exists")
raise ResendAPIError(f"Duplicate resource: {str(error)}")
elif status_code == 429:
logger.warning("Rate limited: Implement retry")
raise RateLimitError("Rate limit exceeded. Retry after delay.")
elif status_code in [500, 502, 503]:
logger.error(f"Server error ({status_code}): Retry recommended")
raise ServerError(f"Server error ({status_code}). Retry in progress...")
else:
logger.error(f"Unknown error: {str(error)}")
raise ResendAPIError(f"API error: {str(error)}")
Resilient retry logic with exponential backoff for transient failures:
interface RetryOptions {
maxRetries?: number;
initialDelayMs?: number;
maxDelayMs?: number;
backoffMultiplier?: number;
retryableStatusCodes?: number[];
}
const DEFAULT_RETRY_OPTIONS: Required<RetryOptions> = {
maxRetries: 5,
initialDelayMs: 100,
maxDelayMs: 30000,
backoffMultiplier: 2,
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
};
async function withExponentialBackoff<T>(
fn: () => Promise<T>,
options: RetryOptions = {}
): Promise<T> {
const config = { ...DEFAULT_RETRY_OPTIONS, ...options };
let lastError: Error | null = null;
let delayMs = config.initialDelayMs;
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
lastError = error;
// Check if error is retryable
const isRetryable =
(error.response?.status &&
config.retryableStatusCodes.includes(error.response.status)) ||
error.code === 'ECONNREFUSED' ||
error.code === 'ETIMEDOUT';
if (!isRetryable || attempt === config.maxRetries) {
throw error;
}
// Calculate delay with jitter
const jitter = Math.random() * 0.1 * delayMs;
const actualDelay = Math.min(delayMs + jitter, config.maxDelayMs);
console.warn(
`Attempt ${attempt + 1} failed. Retrying in ${actualDelay.toFixed(0)}ms...`,
error.message
);
await new Promise(resolve => setTimeout(resolve, actualDelay));
delayMs = Math.min(delayMs * config.backoffMultiplier, config.maxDelayMs);
}
}
throw lastError || new Error('Retry loop exhausted');
}
// Usage
async function sendEmailWithRetry(payload: any) {
return withExponentialBackoff(
() => resend.emails.send(payload),
{
maxRetries: 5,
initialDelayMs: 100,
maxDelayMs: 10000,
backoffMultiplier: 2,
}
);
}
import asyncio
import random
import logging
from typing import TypeVar, Callable, Coroutine, Optional, List
from enum import Enum
logger = logging.getLogger(__name__)
T = TypeVar('T')
class RetryStrategy(Enum):
EXPONENTIAL = "exponential"
LINEAR = "linear"
async def with_exponential_backoff(
fn: Callable[[], Coroutine],
max_retries: int = 5,
initial_delay_ms: float = 100,
max_delay_ms: float = 30000,
backoff_multiplier: float = 2,
retryable_status_codes: Optional[List[int]] = None,
) -> T:
"""
Execute async function with exponential backoff retry
Args:
fn: Async function to execute
max_retries: Maximum retry attempts
initial_delay_ms: Initial delay in milliseconds
max_delay_ms: Maximum delay in milliseconds
backoff_multiplier: Multiplier for each retry
retryable_status_codes: HTTP status codes to retry on
Returns:
Result from function call
Raises:
Exception: If all retries exhausted
"""
if retryable_status_codes is None:
retryable_status_codes = [408, 429, 500, 502, 503, 504]
last_error = None
delay_ms = initial_delay_ms
for attempt in range(max_retries + 1):
try:
return await fn()
except Exception as error:
last_error = error
# Check if error is retryable
is_retryable = (
(hasattr(error, 'status_code') and
error.status_code in retryable_status_codes) or
'timeout' in str(error).lower() or
'connection' in str(error).lower()
)
if not is_retryable or attempt == max_retries:
raise error
# Calculate delay with jitter
jitter = random.random() * 0.1 * delay_ms
actual_delay = min(delay_ms + jitter, max_delay_ms)
logger.warning(
f"Attempt {attempt + 1} failed. "
f"Retrying in {actual_delay:.0f}ms... Error: {str(error)}"
)
await asyncio.sleep(actual_delay / 1000)
delay_ms = min(delay_ms * backoff_multiplier, max_delay_ms)
if last_error:
raise last_error
raise Exception("Retry loop exhausted")
Validate payloads before sending to catch errors early:
interface EmailPayload {
from: string;
to: string | string[];
subject: string;
html?: string;
text?: string;
cc?: string[];
bcc?: string[];
reply_to?: string;
attachments?: Array<{ filename: string; content: Buffer | string }>;
scheduled_at?: string;
tags?: Array<{ name: string; value: string }>;
}
function validateEmailPayload(payload: any): { valid: boolean; errors: string[] } {
const errors: string[] = [];
// Check required fields
if (!payload.from) {
errors.push("'from' field is required");
}
if (!payload.to) {
errors.push("'to' field is required");
}
if (!payload.subject) {
errors.push("'subject' field is required");
}
// Validate email addresses
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (payload.from && !emailRegex.test(payload.from)) {
errors.push(`Invalid 'from' email: ${payload.from}`);
}
const recipients = Array.isArray(payload.to) ? payload.to : [payload.to];
recipients.forEach((email: string) => {
if (!emailRegex.test(email)) {
errors.push(`Invalid recipient email: ${email}`);
}
});
// Validate optional email fields
if (payload.reply_to && !emailRegex.test(payload.reply_to)) {
errors.push(`Invalid 'reply_to' email: ${payload.reply_to}`);
}
if (payload.cc) {
payload.cc.forEach((email: string) => {
if (!emailRegex.test(email)) {
errors.push(`Invalid 'cc' email: ${email}`);
}
});
}
if (payload.bcc) {
payload.bcc.forEach((email: string) => {
if (!emailRegex.test(email)) {
errors.push(`Invalid 'bcc' email: ${email}`);
}
});
}
// Validate attachment content
if (payload.attachments) {
payload.attachments.forEach((att: any, index: number) => {
if (!att.filename) {
errors.push(`Attachment ${index} missing 'filename'`);
}
if (!att.content) {
errors.push(`Attachment ${index} missing 'content'`);
}
});
}
// Validate scheduled_at format if present
if (payload.scheduled_at) {
try {
new Date(payload.scheduled_at);
} catch {
errors.push(`Invalid 'scheduled_at' format: ${payload.scheduled_at}`);
}
}
return {
valid: errors.length === 0,
errors,
};
}
// Usage
async function sendEmailSafely(payload: EmailPayload) {
const validation = validateEmailPayload(payload);
if (!validation.valid) {
console.error('Validation errors:', validation.errors);
throw new Error(`Payload validation failed: ${validation.errors.join(', ')}`);
}
return resend.emails.send(payload);
}
import re
from typing import Dict, List, Any, Tuple
from datetime import datetime
class EmailPayloadValidator:
EMAIL_REGEX = r'^[^\s@]+@[^\s@]+\.[^\s@]+$'
@staticmethod
def validate_email(email: str) -> bool:
"""Validate email address format"""
return re.match(EmailPayloadValidator.EMAIL_REGEX, email) is not None
@staticmethod
def validate_payload(payload: Dict[str, Any]) -> Tuple[bool, List[str]]:
"""
Validate email payload before sending
Returns:
Tuple of (is_valid, error_list)
"""
errors = []
# Check required fields
if not payload.get('from'):
errors.append("'from' field is required")
if not payload.get('to'):
errors.append("'to' field is required")
if not payload.get('subject'):
errors.append("'subject' field is required")
# Validate email addresses
if payload.get('from'):
if not EmailPayloadValidator.validate_email(payload['from']):
errors.append(f"Invalid 'from' email: {payload['from']}")
# Validate recipients
recipients = payload.get('to', [])
if isinstance(recipients, str):
recipients = [recipients]
for email in recipients:
if not EmailPayloadValidator.validate_email(email):
errors.append(f"Invalid recipient email: {email}")
# Validate optional email fields
if payload.get('reply_to'):
if not EmailPayloadValidator.validate_email(payload['reply_to']):
errors.append(f"Invalid 'reply_to' email: {payload['reply_to']}")
for cc_email in payload.get('cc', []):
if not EmailPayloadValidator.validate_email(cc_email):
errors.append(f"Invalid 'cc' email: {cc_email}")
for bcc_email in payload.get('bcc', []):
if not EmailPayloadValidator.validate_email(bcc_email):
errors.append(f"Invalid 'bcc' email: {bcc_email}")
# Validate attachments
for i, attachment in enumerate(payload.get('attachments', [])):
if not attachment.get('filename'):
errors.append(f"Attachment {i} missing 'filename'")
if not attachment.get('content'):
errors.append(f"Attachment {i} missing 'content'")
# Validate scheduled_at format
if payload.get('scheduled_at'):
try:
datetime.fromisoformat(
payload['scheduled_at'].replace('Z', '+00:00')
)
except ValueError:
errors.append(f"Invalid 'scheduled_at' format: {payload['scheduled_at']}")
return len(errors) == 0, errors
def send_email_safely(client, payload: Dict[str, Any]):
"""Send email with validation"""
is_valid, errors = EmailPayloadValidator.validate_payload(payload)
if not is_valid:
raise ValueError(f"Payload validation failed: {', '.join(errors)}")
return client.emails.send(payload)
# .env
RESEND_API_KEY=your_resend_api_key_here
# Resend API Configuration
RESEND_API_KEY=your_resend_api_key_here
# Optional: Custom request timeout (ms)
RESEND_REQUEST_TIMEOUT=30000
# Optional: Max retries for transient failures
RESEND_MAX_RETRIES=5
# Optional: Rate limit requests per second
RESEND_RATE_LIMIT=2
npm install resend
# or
yarn add resend
# or
pnpm add resend
pip install resend
// CORRECT
const apiKey = process.env.RESEND_API_KEY;
// WRONG - Never hardcode
const apiKey = 're_abc123xyz...';
async function sendEmailWithFallback(payload: EmailPayload) {
try {
return await resend.emails.send(payload);
} catch (error) {
// Log error for monitoring
console.error('Email send failed:', error);
// Implement fallback behavior
// - Queue for retry
// - Alert administrator
// - Use alternative service
throw error;
}
}
interface APIMetrics {
successCount: number;
failureCount: number;
rateLimitHits: number;
averageResponseTime: number;
}
class APIMonitor {
private metrics: APIMetrics = {
successCount: 0,
failureCount: 0,
rateLimitHits: 0,
averageResponseTime: 0,
};
recordSuccess(duration: number) {
this.metrics.successCount++;
this.updateAverageResponseTime(duration);
}
recordFailure(error: any) {
this.metrics.failureCount++;
if (error?.response?.status === 429) {
this.metrics.rateLimitHits++;
}
}
private updateAverageResponseTime(duration: number) {
const totalRequests = this.metrics.successCount + this.metrics.failureCount;
const currentAvg = this.metrics.averageResponseTime;
this.metrics.averageResponseTime =
(currentAvg * (totalRequests - 1) + duration) / totalRequests;
}
getMetrics(): APIMetrics {
return { ...this.metrics };
}
}
enum CircuitState {
CLOSED = 'CLOSED',
OPEN = 'OPEN',
HALF_OPEN = 'HALF_OPEN',
}
class CircuitBreaker {
private state: CircuitState = CircuitState.CLOSED;
private failureCount = 0;
private failureThreshold = 5;
private resetTimeout = 60000; // 1 minute
private lastFailureTime = 0;
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === CircuitState.OPEN) {
if (Date.now() - this.lastFailureTime > this.resetTimeout) {
this.state = CircuitState.HALF_OPEN;
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failureCount = 0;
this.state = CircuitState.CLOSED;
}
private onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = CircuitState.OPEN;
}
}
}
typescript-client/ - Complete TypeScript client setup with authenticationpython-client/ - Complete Python client setup with authenticationerror-handling/ - Comprehensive error handling and recovery patternsSee individual example README files for complete code and usage patterns.
This skill follows strict security rules:
.gitignore protection documented