Resend
Transports

Resend

Send emails via the Resend HTTP API

The Resend transport is an official Better-Notify package that sends email through the Resend HTTP API. It uses plain fetch() with zero external dependencies, so it works in Node.js, Bun, Cloudflare Workers, and any runtime with a global fetch.

Install

npm install @betternotify/resend @betternotify/core @betternotify/email

Getting your API key

  1. Sign up or log in at resend.com.
  2. Go to API Keys in the sidebar.
  3. Click Create API Key, give it a name, and select Sending access with the domain(s) you want to send from.
  4. Copy the key — it starts with re_ and is only shown once.

Store it as an environment variable:

RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxx

You must verify your domain before sending to addresses outside of delivered@resend.dev.

Usage

import { createNotify, createClient } from '@betternotify/core';
import { emailChannel } from '@betternotify/email';
import { resendTransport } from '@betternotify/resend';

const email = emailChannel({
  defaults: { from: { name: 'My App', email: 'noreply@example.com' } },
});

const rpc = createNotify({ channels: { email } });
const catalog = rpc.catalog({
  /* routes */
});

const mail = createClient({
  catalog,
  transportsByChannel: {
    email: resendTransport({
      apiKey: process.env.RESEND_API_KEY!,
    }),
  },
});

Options

Prop

Type

Error handling

The transport maps Resend HTTP status codes to Better-Notify error codes:

HTTP statusResend errorBetter-Notify code
422missing_required_field, invalid_from_address, invalid_attachmentVALIDATION
401missing_api_keyCONFIG
403invalid_api_key, restricted_api_keyCONFIG
429rate_limit_exceeded, daily_quota_exceededPROVIDER
500internal_server_errorPROVIDER

When a 429 response includes a Retry-After header, the value is included in the error message.

Network failures and unparseable responses are wrapped as PROVIDER errors. Timeouts are wrapped as TIMEOUT errors.

Tags

Resend supports email tags for tracking and analytics. Better-Notify's tags field (a Record<string, string | number | boolean>) is converted to Resend's Array<{ name, value }> format automatically:

await mail.welcome.send({
  to: 'user@example.com',
  input: { name: 'Alice' },
  tags: { campaign: 'onboarding', version: 2 },
});
// Sent as: [{ name: "campaign", value: "onboarding" }, { name: "version", value: "2" }]

Tag names and values must be ASCII alphanumeric, underscores, or dashes (max 256 characters each). Invalid tags are rejected by Resend with a 422 error.

Attachments

The transport base64-encodes attachment content before sending:

  • contentType maps to Resend's content_type field (optional — Resend infers it when omitted).
  • cid maps to content_id for inline images.
await mail.invoice.send({
  to: 'user@example.com',
  input: { orderId: '12345' },
  attachments: [
    { filename: 'invoice.pdf', content: pdfBuffer, contentType: 'application/pdf' },
  ],
});

Limits

These limits are enforced by Resend, not by the transport:

  • 50 recipients per email (to + cc + bcc combined)
  • 40 MB total attachment size per email
  • Daily and monthly sending limits depend on your Resend plan
  • Tag names and values: ASCII alphanumeric, underscores, dashes, max 256 characters

If you need throttling or provider fallback, add withRateLimit middleware or wrap the transport in multiTransport({ strategy: 'failover' }).

On this page