Skip to main content
Business & GrowthNewsletter Marketing Services294 lines

Resend

"Resend: developer-first email API, transactional and marketing emails, React Email templates, domains, audiences, broadcasts, REST API"

Quick Summary16 lines
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 lines
Paste into your CLAUDE.md or agent config

Resend — 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

Get CLI access →