---
title: "Resend"
description: "Use Resend for MainSDK email and the notify email channel - domain verification, API keys, main.yaml, local testing and swapping."
section: "integrations"
canonical: "https://mainsdk.dev/docs/integrations/resend"
updated: "2026-10-28"
---

# Resend

> Use Resend for MainSDK email and the notify email channel - domain verification, API keys, main.yaml, local testing and swapping.

Canonical: https://mainsdk.dev/docs/integrations/resend · Updated: 2026-10-28

Resend is the default provider behind [`email`](https://mainsdk.dev/docs/interfaces/email). Your app sends receipts, sign-up confirmations and newsletters with `email.send()` and `email.marketing()`, and Resend delivers them from your own domain. The same driver carries the email channel of [`notify`](https://mainsdk.dev/docs/interfaces/notify) when a person is best reached by email. Pick Resend when you want the default path with the least setup; pick Amazon SES when you already run on AWS and want sending inside your AWS account.

## Interfaces it serves

### Notifications (`notify`)

- Status: Building for 0.3 · ships as a stable API
- Driver: `resend` (Resend, planned driver)
- Docs: https://mainsdk.dev/docs/interfaces/notify.md

```yaml
notify:
  driver: resend
```

Swap to another provider: `whatsapp-cloud`, `bird`, `msg91`, `twilio`, `local`, `fcm`, `apns`, `expo`, `ses`, `onesignal`, `slack`, `pagerduty`, `opsgenie`, `webhook`.

### Email (`email`)

- Status: Building for 0.3 · ships as a stable API
- Driver: `resend` (Resend, default)
- Docs: https://mainsdk.dev/docs/interfaces/email.md

```yaml
email:
  driver: resend
```

Swap to another provider: `ses`, `local`, `postmark`, `bird`, `smtp`.

## Before you start

You need a Resend account and a domain you can add DNS records to.

1. **Add your sending domain.** In the Resend dashboard, add the domain (or a subdomain such as `mail.pinecart.dev`) your app sends from. Resend shows the DNS records it needs, typically for SPF and DKIM. Using a subdomain keeps your app's mail apart from your team's own mail.
2. **Add the DNS records at your DNS host** and wait for Resend to show the domain as verified. Mail from an unverified domain is refused. A DMARC record on the parent domain is good practice for deliverability.
3. **Create an API key.** In the API keys area, create a key for this app. Resend lets you limit a key to sending only, and to one domain; use the narrowest key that works.
4. **Use separate keys per environment.** A key for development and stage, another for production, so you can revoke one without touching the other.

Store the keys in your secrets manager, never in code or a committed env file.

## Configure

```yaml title="main.yaml"
email:
  driver: resend
  key: secret:RESEND_API_KEY              # the Resend API key
  from: "Pinecart <hello@mail.pinecart.dev>"   # on the domain you verified
  reply-to: support@pinecart.dev
  webhook: resend-events                  # bounces and complaints, see below
  tracking:
    opens: false                          # the default: no tracking pixel
    clicks: false                         # the default: no rewritten links
webhooks:
  inbound:
    resend-events:
      preset: standard                    # Resend signs webhooks with Svix, which follows Standard Webhooks
      secret: secret:RESEND_WEBHOOK_SECRET
      path: /api/webhooks/resend
```

Put the production key in an environment overlay so only the difference changes:

```yaml title="main.env.prod.yaml"
email:
  key: secret:RESEND_PROD_API_KEY   # suggested name
```

A literal key in `main.yaml` fails to load with `MAIN-CONFIG-0004`. Without `email.key`, the driver reads `secret:RESEND_API_KEY`.

> **Warning:** The Resend request and webhook shapes are written from Resend's documentation and are unverified against the live API. Verify with a sandbox key before production: send one message to Resend's test recipients and post one test webhook.

### What the driver sends

Each message is one `POST https://api.resend.com/emails` with `Authorization: Bearer <key>` and an `Idempotency-Key` header. The JSON body holds `from`, `to`, `subject`, `text`, `html` when the template has one, `reply_to`, `headers` (the `List-Unsubscribe` pair on marketing mail), and `tags` for the template, the kind (`transactional` or `marketing`), the campaign and the tenant. The call goes through MainSDK's network layer, does not follow redirects, and reads at most 64 KiB of the answer. A 429, a 5xx or no answer is a retryable `MAIN-EMAIL-0004`, so a `fallback` driver is tried; any other refusal is not retryable. The message and the provider's reply are not put in the error.

### Bounces and complaints

Add a webhook in the Resend dashboard that points at your `path` with the events `email.bounced`, `email.complained` and `email.delivered`, and put its signing secret in `secret:RESEND_WEBHOOK_SECRET`. MainSDK checks the signature, then reads each event: a permanent bounce (any type but `Transient`) and a complaint add the address to the suppression list, a transient bounce does not, and a delivery is only reported to `email.on` listeners. The tags tie each event to its campaign and tenant. Without `email.webhook` in production, `mainsdk check` warns with `MAIN-EMAIL-0010`.

To reach people by email through `notify`, add `email` to the channel list. The email channel uses your `email` block, so there is nothing else to configure:

```yaml title="main.yaml"
notify:
  channels: [whatsapp, sms, email]   # email is tried last
```

## Use it

Send a transactional email. The idempotency key ties it to the order, so a retried action sends once.

```ts title="app/orders/receipt.ts"
import { email } from "mainsdk";

export async function sendReceipt(user: { email: string }, orderId: string) {
  const { id } = await email.send({
    to: user.email,
    template: "receipt",
    data: { orderId },
    idempotencyKey: `receipt:${orderId}`,
  });
  return id;
}
```

Notify a person on whichever channel they allow; Resend delivers it when email is the channel chosen.

```ts title="app/orders/ship.ts"
import { notify } from "mainsdk";

await notify(user, {
  template: "order.shipped",
  data: { orderId: order.id },
  category: "orders",
});
```

## Local development and testing

- **No Resend account:** `mainsdk dev` sends every email to the local inbox, where you can see the rendered message and its headers.
- **Resend before your domain is ready:** Resend provides test recipient addresses that simulate delivered, bounced and complained outcomes, which is a safe way to see how bounces reach your suppression list.
- **Tests:** `createTestApp()` from `@mainsdk/testing` runs `email` on the local mailbox, which enforces the unsubscribe rule and the suppression list. `mailbox(app).failNext(1)` simulates an outage, for example to prove a `fallback` to SES. `fakeMessagingServer("resend")` records the request the real driver makes, with no network.

```ts title="app/orders/receipt.test.ts"
import { it } from "node:test";
import { createTestApp, mailbox } from "@mainsdk/testing";
import { sendReceipt } from "./receipt";

it("sends one receipt", async () => {
  const app = createTestApp({ main: "./main.yaml" });
  await sendReceipt({ email: "a@pinecart.dev" }, "ord_1");
  mailbox(app).toHaveSent({ to: "a@pinecart.dev", template: "receipt", times: 1 });
});
```

`mainsdk check --env prod` fails with `MAIN-CHECK-0026` if `email` still resolves to the local driver.

## What MainSDK adds on top

- Marketing email does not compile without an unsubscribe, and every marketing message carries one-click `List-Unsubscribe` headers.
- Unsubscribes, hard bounces and complaints go to a suppression list that every later send checks, whichever provider sends.
- No open-tracking pixels or rewritten links unless you turn them on in `main.yaml`.
- A retry with the same idempotency key never sends twice.

See [Email](https://mainsdk.dev/docs/interfaces/email) for the full contract.

## Limits and notes

- The `email:resend` driver is built (spec `email-notify`). `notify` has no Resend driver of its own: its email channel uses your `email` block.
- Tracking: with `tracking.opens` off, MainSDK removes tracking pixels from the HTML before it reaches Resend.
- Resend sends from the regions it offers for each domain. Check that they fit your MainSDK data region (US, EU or India).
- The Resend driver declares its own host (`api.resend.com`), so your `network.allow` list doesn't change. The signed webhook path is exempt from CSRF automatically.
- `email.raw()` returns the Resend client for features `email` does not wrap. `mainsdk check` lists every use.

## Swap to or from it

```yaml title="main.yaml"
email:
  driver: ses   # was resend
```

Point `key` at the new provider's credential. Email is stateless: your calls, templates and the suppression list stay the same, and `mainsdk swap` can run the contract tests and a shadow send before switching (see [Swap a driver](https://mainsdk.dev/docs/guides/swap-a-driver)). Verify your sending domain with the new provider first, or its sends are refused. To fail over instead of swapping, list `fallback: { email: [resend, ses] }`.

## FAQ

**Do I need a Resend account to start building?** No. `mainsdk dev` uses the local inbox and tests use the in-memory fake. You need an account and a verified domain to send real mail.

**Can I track opens with Resend?** Yes, by setting `tracking.opens: true` in `main.yaml`. It is off by default, and `mainsdk info` lists it when on.

**Does Resend keep my unsubscribe list?** You don't rely on it. MainSDK keeps the suppression list and checks it before any send reaches Resend, so it survives a provider swap.

## Next

- [Email](https://mainsdk.dev/docs/interfaces/email) and [Notifications](https://mainsdk.dev/docs/interfaces/notify)
- [Amazon SES](https://mainsdk.dev/docs/integrations/amazon-ses), [Postmark](https://mainsdk.dev/docs/integrations/postmark) and [SMTP](https://mainsdk.dev/docs/integrations/smtp)
- [Swap a driver](https://mainsdk.dev/docs/guides/swap-a-driver)
