Resend
"Resend: developer-first email API, transactional and marketing emails, React Email templates, domains, audiences, broadcasts, REST API"
You are an expert in integrating Resend for transactional email and newsletter delivery. ## Key Points - **Use React Email for templates**: Resend is built to work with React Email. Use component-based templates instead of raw HTML strings for maintainability and type safety. - **Verify domains before sending**: Always verify your sending domain via DNS records. Emails from unverified domains will fail or land in spam. - **Handle webhook events for deliverability**: Set up webhooks for bounce and complaint events, and automatically suppress those addresses to protect your sender reputation. - **Ignoring rate limits on free tier**: The free plan allows only 100 emails/day and 10/second. Bulk sends without throttling will hit 429 errors immediately. ## Quick Example ```bash npm install resend ```
skilldb get newsletter-marketing-services-skills/ResendFull skill: 294 linesResend — Newsletter & Email Marketing
You are an expert in integrating Resend for transactional email and newsletter delivery.
Core Philosophy
Resend is a developer-first email platform built for modern applications. Its REST API lives at https://api.resend.com/. Authentication uses a Bearer token. Resend differentiates itself with first-class support for React Email templates, a simple SDK, and the Audiences + Broadcasts feature for newsletter-style campaigns. The API is straightforward: send individual emails, manage contacts in audiences, and trigger broadcasts. Rate limits are tier-based (starting at 10 emails/second on free plans). Design integrations that use the SDK for sending, React Email for templates, and the Audiences API for subscriber management.
Setup & Configuration
SDK Installation and Client Setup
npm install resend
import { Resend } from "resend";
const resend = new Resend(process.env.RESEND_API_KEY);
// Verify connection
async function verifyConnection(): Promise<void> {
const { data } = await resend.domains.list();
console.log(`Connected. Verified domains: ${data?.length ?? 0}`);
}
Domain Verification
async function addDomain(domain: string): Promise<{
id: string;
records: Array<{ type: string; name: string; value: string }>;
}> {
const { data, error } = await resend.domains.create({ name: domain });
if (error) throw new Error(`Domain creation failed: ${error.message}`);
console.log("Add these DNS records to verify your domain:");
for (const record of data!.records) {
console.log(` ${record.type}: ${record.name} -> ${record.value}`);
}
return data!;
}
async function verifyDomain(domainId: string): Promise<void> {
const { error } = await resend.domains.verify(domainId);
if (error) throw new Error(`Verification failed: ${error.message}`);
console.log("Domain verification initiated. Check DNS propagation.");
}
Core Patterns
Sending Emails
async function sendEmail(opts: {
from: string;
to: string | string[];
subject: string;
html: string;
replyTo?: string;
tags?: Array<{ name: string; value: string }>;
}): Promise<{ id: string }> {
const { data, error } = await resend.emails.send({
from: opts.from,
to: opts.to,
subject: opts.subject,
html: opts.html,
reply_to: opts.replyTo,
tags: opts.tags,
});
if (error) throw new Error(`Send failed: ${error.message}`);
return { id: data!.id };
}
// Send with React Email component (server-side)
import { render } from "@react-email/render";
import { WelcomeEmail } from "./emails/welcome";
async function sendWelcomeEmail(
to: string,
name: string
): Promise<{ id: string }> {
const html = await render(WelcomeEmail({ name }));
return sendEmail({
from: "welcome@yourdomain.com",
to,
subject: `Welcome, ${name}!`,
html,
tags: [{ name: "category", value: "onboarding" }],
});
}
Audience and Contact Management
interface Contact {
id: string;
email: string;
first_name?: string;
last_name?: string;
unsubscribed: boolean;
created_at: string;
}
async function createAudience(
name: string
): Promise<{ id: string; name: string }> {
const { data, error } = await resend.audiences.create({ name });
if (error) throw new Error(`Audience creation failed: ${error.message}`);
return data!;
}
async function addContact(
audienceId: string,
contact: {
email: string;
firstName?: string;
lastName?: string;
unsubscribed?: boolean;
}
): Promise<Contact> {
const { data, error } = await resend.contacts.create({
audience_id: audienceId,
email: contact.email,
first_name: contact.firstName,
last_name: contact.lastName,
unsubscribed: contact.unsubscribed ?? false,
});
if (error) throw new Error(`Contact creation failed: ${error.message}`);
return data as unknown as Contact;
}
async function removeContact(
audienceId: string,
contactId: string
): Promise<void> {
const { error } = await resend.contacts.remove({
audience_id: audienceId,
id: contactId,
});
if (error) throw new Error(`Contact removal failed: ${error.message}`);
}
async function listContacts(
audienceId: string
): Promise<Contact[]> {
const { data, error } = await resend.contacts.list({
audience_id: audienceId,
});
if (error) throw new Error(`List contacts failed: ${error.message}`);
return (data?.data ?? []) as unknown as Contact[];
}
Bulk Subscriber Import
async function bulkImportContacts(
audienceId: string,
contacts: Array<{ email: string; firstName?: string; lastName?: string }>
): Promise<{ imported: number; failed: number }> {
let imported = 0;
let failed = 0;
// Resend does not have a bulk endpoint; batch sequentially with concurrency control
const concurrency = 5;
for (let i = 0; i < contacts.length; i += concurrency) {
const batch = contacts.slice(i, i + concurrency);
const results = await Promise.allSettled(
batch.map((c) =>
addContact(audienceId, {
email: c.email,
firstName: c.firstName,
lastName: c.lastName,
})
)
);
for (const r of results) {
if (r.status === "fulfilled") imported++;
else failed++;
}
}
return { imported, failed };
}
Broadcast (Newsletter Send to Audience)
async function sendBroadcast(opts: {
audienceId: string;
from: string;
subject: string;
html: string;
replyTo?: string;
}): Promise<{ id: string }> {
// Broadcasts use the Resend broadcast API
const { data, error } = await resend.broadcasts.create({
audience_id: opts.audienceId,
from: opts.from,
subject: opts.subject,
html: opts.html,
reply_to: opts.replyTo,
});
if (error) throw new Error(`Broadcast creation failed: ${error.message}`);
// Send the broadcast
const sendResult = await resend.broadcasts.send(data!.id);
if (sendResult.error) {
throw new Error(`Broadcast send failed: ${sendResult.error.message}`);
}
return { id: data!.id };
}
Webhook Event Handling
import type { IncomingMessage } from "http";
type ResendEvent =
| "email.sent"
| "email.delivered"
| "email.bounced"
| "email.complained"
| "email.opened"
| "email.clicked";
interface ResendWebhookPayload {
type: ResendEvent;
created_at: string;
data: {
email_id: string;
from: string;
to: string[];
subject: string;
};
}
async function handleResendWebhook(
payload: ResendWebhookPayload
): Promise<void> {
switch (payload.type) {
case "email.bounced":
console.log(`Bounce: ${payload.data.to.join(", ")}`);
// Mark contact as bounced in your database
break;
case "email.complained":
console.log(`Complaint: ${payload.data.to.join(", ")}`);
// Unsubscribe the contact immediately
break;
case "email.delivered":
console.log(`Delivered: ${payload.data.email_id}`);
break;
default:
console.log(`Event: ${payload.type}`);
}
}
Best Practices
- Use React Email for templates: Resend is built to work with React Email. Use component-based templates instead of raw HTML strings for maintainability and type safety.
- Verify domains before sending: Always verify your sending domain via DNS records. Emails from unverified domains will fail or land in spam.
- Handle webhook events for deliverability: Set up webhooks for bounce and complaint events, and automatically suppress those addresses to protect your sender reputation.
Common Pitfalls
- Ignoring rate limits on free tier: The free plan allows only 100 emails/day and 10/second. Bulk sends without throttling will hit 429 errors immediately.
- Using Resend for high-volume marketing without Audiences: Sending marketing emails via the single-send endpoint is inefficient and does not handle unsubscribes. Use the Audiences and Broadcasts features for newsletter workflows.
Anti-Patterns
Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.
Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.
Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.
Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.
Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.
Install this skill directly: skilldb add newsletter-marketing-services-skills
Related Skills
Beehiiv
"Beehiiv: newsletter platform API, subscriber management, publications, posts/campaigns, automations, referral program, analytics, REST API"
Buttondown
"Buttondown: minimal newsletter API, subscribers, emails/drafts, tags, automations, webhooks, Markdown-first, REST API"
ConvertKit (Kit)
"ConvertKit (Kit): creator email platform, subscriber management, forms, sequences, broadcasts, tags, automations, commerce, REST API"
Loops
"Loops: email for SaaS, transactional + marketing, contacts, events, loops (automations), API sends, webhooks"
Mailchimp
"Mailchimp: email marketing platform API, audience lists, campaigns, automations, segments, templates, analytics, REST API v3"
MailerLite
"MailerLite: email marketing platform API, subscriber groups, campaigns, automations, forms, analytics, REST API v2"