Skill v1.0.1
currentAutomated scan100/100Add Labs status badges to README (#264)
version: "1.0.1" name: resend description: Emulated Resend email API for local development and testing. Use when the user needs to send emails locally, test transactional email flows, implement magic link or verification code auth, inspect sent emails, manage domains/contacts/API keys, or work with the Resend API without sending real emails. Triggers include "Resend API", "emulate Resend", "send email locally", "test email", "magic link", "verification email", "email inbox", "RESEND_BASE_URL", or any task requiring a local email API. allowed-tools: Bash(npx emulate:), Bash(curl:)
Resend Email API Emulator
Fully stateful Resend API emulation. Emails, domains, API keys, audiences, and contacts persist in memory. Sent emails are captured and viewable through the inbox UI or the REST API.
No real emails are sent. Every call to POST /emails stores the message locally so you can inspect it programmatically or in the browser.
Start
# Resend onlynpx emulate --service resend# Default port (when run alone)# http://localhost:4000
Or programmatically:
import { createEmulator } from 'emulate'const resend = await createEmulator({ service: 'resend', port: 4000 })// resend.url === 'http://localhost:4000'
Auth
Pass tokens as Authorization: Bearer <token>. Any re_ prefixed token is accepted.
curl http://localhost:4000/emails \-H "Authorization: Bearer re_test_key"
When no token is provided, requests fall back to the default user.
Pointing Your App at the Emulator
Environment Variable (Resend SDK)
The official Resend Node.js SDK reads RESEND_BASE_URL at module load time. Set it to the emulator URL and the SDK works without any code changes:
RESEND_BASE_URL=http://localhost:4000
import { Resend } from 'resend'// No baseUrl argument needed; the SDK reads RESEND_BASE_URL automatically.const resend = new Resend('re_test_key')await resend.emails.send({from: 'hello@example.com',to: 'user@example.com',subject: 'Hello',html: '<p>It works!</p>',})
Embedded in Next.js (adapter-next)
When using @emulators/adapter-next, the emulator runs inside your Next.js app at /emulate/resend. Set RESEND_BASE_URL via next.config.ts:
// next.config.tsimport { withEmulate } from '@emulators/adapter-next'export default withEmulate({env: {RESEND_BASE_URL: `http://localhost:${process.env.PORT ?? '3000'}/emulate/resend`,},})
// app/emulate/[...path]/route.tsimport { createEmulateHandler } from '@emulators/adapter-next'import * as resend from '@emulators/resend'export const { GET, POST, PUT, PATCH, DELETE } = createEmulateHandler({services: {resend: {emulator: resend,seed: {domains: [{ name: 'example.com' }],},},},})
Direct fetch
If you cannot use the SDK or env var, call the emulator directly:
await fetch('http://localhost:4000/emails', {method: 'POST',headers: {'Content-Type': 'application/json','Authorization': 'Bearer re_test_key',},body: JSON.stringify({from: 'hello@example.com',to: 'user@example.com',subject: 'Hello',html: '<p>It works!</p>',}),})
Seed Config
resend:domains:- name: example.comregion: us-east-1contacts:- email: test@example.comfirst_name: Testlast_name: Useraudience: Default
Retrieving Sent Emails
This is the key differentiator of the emulator: every email sent via POST /emails is stored and queryable.
Inbox UI
Browse sent emails in the browser:
http://localhost:4000/inbox
REST API
# List all sent emailscurl http://localhost:4000/emails \-H "Authorization: Bearer re_test_key"# Get a single email by IDcurl http://localhost:4000/emails/<id> \-H "Authorization: Bearer re_test_key"
Extracting Data from Emails (tests, agents)
Useful for completing magic link, verification code, or password reset flows programmatically:
# Get the latest email IDEMAIL_ID=$(curl -s http://localhost:4000/emails \-H "Authorization: Bearer re_test_key" | jq -r '.data[0].id')# Extract a 6-digit code from the HTML bodyCODE=$(curl -s http://localhost:4000/emails/$EMAIL_ID \-H "Authorization: Bearer re_test_key" | jq -r '.html' | grep -oE '[0-9]{6}')echo "Verification code: $CODE"
API Endpoints
Emails
# Send an emailcurl -X POST http://localhost:4000/emails \-H "Authorization: Bearer $TOKEN" \-H "Content-Type: application/json" \-d '{"from": "hello@example.com", "to": "user@example.com", "subject": "Hello", "html": "<p>Hi</p>"}'# Send batch (up to 100)curl -X POST http://localhost:4000/emails/batch \-H "Authorization: Bearer $TOKEN" \-H "Content-Type: application/json" \-d '[{"from": "a@example.com", "to": "b@example.com", "subject": "One", "html": "<p>1</p>"}]'# List all emailscurl http://localhost:4000/emails \-H "Authorization: Bearer $TOKEN"# Get email by IDcurl http://localhost:4000/emails/<id> \-H "Authorization: Bearer $TOKEN"# Cancel a scheduled emailcurl -X POST http://localhost:4000/emails/<id>/cancel \-H "Authorization: Bearer $TOKEN"
Supported fields: from, to, subject, html, text, cc, bcc, reply_to, headers, tags, scheduled_at.
Idempotent Sends
POST /emails and POST /emails/batch accept the case-insensitive Idempotency-Key header. Keys must be 1 to 256 characters and remain active for 24 hours. A retry with the same key and validated payload returns the original response, email IDs, and status without creating emails or dispatching duplicate email.sent and email.delivered webhooks. Reusing a key with a different payload or endpoint returns 409 invalid_idempotent_request; invalid key lengths return 400 invalid_idempotency_key. Omitting the header preserves normal behavior.
curl -X POST http://localhost:4000/emails \-H "Authorization: Bearer $TOKEN" \-H "Content-Type: application/json" \-H "Idempotency-Key: signup-email-123" \-d '{"from": "hello@example.com", "to": "user@example.com", "subject": "Hello"}'
Domains
# Create domaincurl -X POST http://localhost:4000/domains \-H "Authorization: Bearer $TOKEN" \-H "Content-Type: application/json" \-d '{"name": "example.com", "region": "us-east-1"}'# List domainscurl http://localhost:4000/domains \-H "Authorization: Bearer $TOKEN"# Get domaincurl http://localhost:4000/domains/<id> \-H "Authorization: Bearer $TOKEN"# Verify domain (instantly marks all records as verified)curl -X POST http://localhost:4000/domains/<id>/verify \-H "Authorization: Bearer $TOKEN"# Delete domaincurl -X DELETE http://localhost:4000/domains/<id> \-H "Authorization: Bearer $TOKEN"
API Keys
# Create API keycurl -X POST http://localhost:4000/api-keys \-H "Authorization: Bearer $TOKEN" \-H "Content-Type: application/json" \-d '{"name": "Production"}'# List API keyscurl http://localhost:4000/api-keys \-H "Authorization: Bearer $TOKEN"# Delete API keycurl -X DELETE http://localhost:4000/api-keys/<id> \-H "Authorization: Bearer $TOKEN"
Audiences
# Create audiencecurl -X POST http://localhost:4000/audiences \-H "Authorization: Bearer $TOKEN" \-H "Content-Type: application/json" \-d '{"name": "Newsletter"}'# List audiencescurl http://localhost:4000/audiences \-H "Authorization: Bearer $TOKEN"# Delete audiencecurl -X DELETE http://localhost:4000/audiences/<id> \-H "Authorization: Bearer $TOKEN"
Contacts
# Create contact in an audiencecurl -X POST http://localhost:4000/audiences/<audience_id>/contacts \-H "Authorization: Bearer $TOKEN" \-H "Content-Type: application/json" \-d '{"email": "user@example.com", "first_name": "Jane", "last_name": "Doe"}'# List contacts in an audiencecurl http://localhost:4000/audiences/<audience_id>/contacts \-H "Authorization: Bearer $TOKEN"# Delete contactcurl -X DELETE http://localhost:4000/audiences/<audience_id>/contacts/<id> \-H "Authorization: Bearer $TOKEN"
Webhooks
The emulator dispatches webhook events when state changes:
email.sentandemail.deliveredonPOST /emailsdomain.createdanddomain.deletedon domain operationscontact.createdandcontact.deletedon contact operations
Common Patterns
Magic Link / Verification Code Flow
TOKEN="re_test_key"BASE="http://localhost:4000"# 1. Send verification emailcurl -X POST $BASE/emails \-H "Authorization: Bearer $TOKEN" \-H "Content-Type: application/json" \-d '{"from": "auth@example.com", "to": "user@example.com", "subject": "Your code", "html": "<p>Code: <strong>482910</strong></p>"}'# 2. Retrieve the emailEMAIL_ID=$(curl -s $BASE/emails -H "Authorization: Bearer $TOKEN" | jq -r '.data[0].id')# 3. Read the HTML bodycurl -s $BASE/emails/$EMAIL_ID -H "Authorization: Bearer $TOKEN" | jq -r '.html'
Send and Verify in a Test
import { createEmulator } from 'emulate'import { Resend } from 'resend'const emu = await createEmulator({ service: 'resend', port: 4000 })process.env.RESEND_BASE_URL = emu.urlconst resend = new Resend('re_test_key')// Sendawait resend.emails.send({from: 'auth@example.com',to: 'user@test.com',subject: 'Verify',html: '<p>Code: <strong>123456</strong></p>',})// Retrieveconst res = await fetch(`${emu.url}/emails`, {headers: { Authorization: 'Bearer re_test_key' },})const { data: emails } = await res.json()console.log(emails[0].html) // contains "123456"
Custom emulators alongside built-ins
Use npx emulate init --custom inventory to scaffold a third-party API emulator and test. Register it in emulate.config.ts with defineConfig from emulate, alongside built-in entries such as { emulator: "resend" }. Run npx emulate start --watch to reload imports and inspect custom state at the printed /_emulate URL. Successful reloads reset the run to seed. Existing flat seed configs still work; --config selects an explicit file. For authoring and testing third-party API emulators, see https://emulate.dev/docs/custom-emulators.