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:
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
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",
},
],
},
})| Field | What it seeds |
|---|---|
domains | Verified sending domains, with a region. |
keys | Known API keys; a sending_access key may only send, as on Resend. |
webhooks | Where events go, which ones, and the signing secret. |
allowlist | When 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:
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("[email protected]")
await screen.getByRole("button", { name: "Sign up" }).click()
const email = await resendInbox(resend).waitFor("[email protected]")
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 email | What it's for |
|---|---|
subject, text, html | What the email says. |
from, to | Who 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
| Area | Covered |
|---|---|
| Emails | Send one or a batch, read them back, schedule and cancel, tags, idempotency keys. |
| Domains | Add, verify, list and delete sending domains. |
| API keys | Create, list and delete keys, full or sending-only. |
| Webhooks | Manage endpoints, and signed deliveries. |
Faults
| Call | Faults the matrix tries |
|---|---|
POST /emails | 429, 500, a 10-second delay, connection reset, answer lost after Resend sent it |
| Every webhook delivery | arrives 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.
| Event | Sent when |
|---|---|
email.sent | Resend accepts an email. |
email.scheduled | An email is scheduled for later. |
email.delivered | The email reaches the inbox. |
domain.created, domain.deleted | A 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 [email protected]. All 5 match.
Not covered yet
- Bounces, complaints, opens and clicks: the emulator never sends those events
- Audiences, contacts and broadcasts
- Receiving email