dánwòdánwò

Email and messaging

Resend

Sending email, batches and scheduled sends, domains, API keys and signed webhooks on api.resend.com, with every message readable from a test.

danwo's Resend emulator answers api.resend.com. The resend package works against it unchanged. Nothing is delivered anywhere: each email lands in an inbox your test reads.

Set up

Add resendService. Any well-formed re_… key is accepted, so your app keeps its own:

danwo.config.ts
import { appEnvironment, defineConfig, resendService } from "@danwo/sdk"

export default defineConfig({
  testDir: "tests",
  environment: () =>
    appEnvironment({
      app: { command: "npm run dev", url: "http://localhost:3000" },
      services: [resendService()],
    }),
})

resendService also hands its mail to the mail fixture, so a test or an agent step can read a sign-in code with mail.code(address).

Seed it

danwo.config.ts · the seed
resendService({
  seed: {
    domains: [{ name: "example.com" }],
    keys: [{ name: "sending", token: "re_sending_only", permission: "sending_access" }],
    webhooks: [
      {
        endpoint: "http://localhost:3000/api/webhooks/resend",
        events: ["email.sent", "email.delivered"],
        secret: "whsec_danwo",
      },
    ],
  },
})
FieldWhat it seeds
domainsVerified sending domains, with a region.
keysKnown API keys; a sending_access key may only send, as on Resend.
webhooksWhere events go, which ones, and the signing secret.
allowlistWhen set, only these addresses may receive mail, as on a sandboxed account.

Use it in a test

Sign up, then read the code from the email the app sent:

tests/verify-email.danwo.ts
import { type ResendFixtures, resendInbox, test as base } from "@danwo/sdk"

const test = base.extend<ResendFixtures>()

test("verifies the email address", async ({ app, screen, expect, resend }) => {
  await app.open("/signup")
  await screen.getByLabel("Email").fill("ada@example.com")
  await screen.getByRole("button", { name: "Sign up" }).click()

  const email = await resendInbox(resend).waitFor("ada@example.com")
  await screen.getByLabel("Code").fill(email.code() ?? "")
  await screen.getByRole("button", { name: "Verify" }).click()

  await expect(screen.getByText("Email verified")).toBeVisible()
  expect(email.subject).toBe("Your code")
})
On each emailWhat it's for
subject, text, htmlWhat the email says.
from, toWho sent it, and to whom.
code()The one-time code in it.
links()The links in it.

resendInbox(resend).list(to) gives every email sent to an address, oldest first.

What's covered

AreaCovered
EmailsSend one or a batch, read them back, schedule and cancel, tags, idempotency keys.
DomainsAdd, verify, list and delete sending domains.
API keysCreate, list and delete keys, full or sending-only.
WebhooksManage endpoints, and signed deliveries.

Faults

CallFaults the matrix tries
POST /emails429, 500, a 10-second delay, connection reset, answer lost after Resend sent it
Every webhook deliveryarrives twice, arrives 10 minutes late, forged signature, 1 hour stale

The error bodies are Resend's, so the SDK hands your code { data: null, error } as it would for real.

Webhooks

Deliveries are signed the Svix way, so Resend's webhooks.verify accepts them.

EventSent when
email.sentResend accepts an email.
email.scheduledAn email is scheduled for later.
email.deliveredThe email reaches the inbox.
domain.created, domain.deletedA domain is added or removed.

Fidelity

Live. danwo fidelity resend runs 5 scenarios against a real Resend account, sending only to Resend's test address delivered@resend.dev. All 5 match.

Not covered yet

  • Bounces, complaints, opens and clicks: the emulator never sends those events
  • Audiences, contacts and broadcasts
  • Receiving email

Next

On this page

Run it on every pull request

A GitHub check and one comment per pull request, with the flight records attached.

Set up CI