Resend

Lightweight, dependency-free, in-memory Resend REST API fake for testing code that uses the real resend Node.js SDK (and the language-agnostic Resend REST API).

Default port: 4651

Quick start

Start the server:

import { ResendServer } from "./services/resend/src/server.js";

const server = new ResendServer(4651);
await server.start();
// ... run your app/tests ...
await server.stop();

Point the real resend client at it. The Resend SDK reads baseUrl from the options object, so override the base URL to the fake:

import { Resend } from "resend";

const resend = new Resend("re_parlel_test_key", {
  baseUrl: "http://127.0.0.1:4651", // point at the parlel fake
});

const { data, error } = await resend.emails.send({
  from: "Acme <onboarding@resend.dev>",
  to: ["delivered@resend.dev"],
  subject: "hello world",
  html: "<p>it works!</p>",
});
// data.id => a generated UUID, error => null

Every send is captured in memory and can be inspected via the /__parlel/* endpoints (see below).

Implemented operations

All routes require a Authorization: Bearer <key> header (any non-empty bearer token is accepted, matching how a local test key behaves). State is in-memory and ephemeral.

Emails — the surface resend.emails and resend.batch call

Supports Idempotency-Key header on POST /emails: a repeated key replays the original response without creating a new email.

Domains — resend.domains

API keys — resend.apiKeys

Audiences — resend.audiences

Contacts — resend.contacts (nested under an audience)

Broadcasts — resend.broadcasts

Service & inspection operations (parlel extensions, not part of Resend)

Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

FeatureStatus
emails.send / get / update / cancel✅ Supported
batch.send (≤100, no attachments/scheduling)✅ Supported
domains.* (create/list/get/update/verify/remove)✅ Supported
apiKeys.* (create/list/remove)✅ Supported
audiences.* (create/list/get/remove)✅ Supported
contacts.* (create/list/get/update/remove, by id or email)✅ Supported
broadcasts.* (create/list/get/update/send/remove)✅ Supported
Idempotency-Key replay on send✅ Supported
Error envelope { statusCode, name, message } with correct status codes (400/401/404/405/422)✅ Supported
Payload validation (missing fields, invalid from/recipients, attachments, region, permission)✅ Supported
Captured-mail inspection✅ Supported (parlel extension)
created_at exact wire format (space-separated microseconds)◐ Accepted — emitted as a valid ISO 8601 string
React component rendering (react field)◐ Accepted as content, not rendered
Contact custom properties◐ Accepted on create and echoed back on retrieve, not validated
Actual email delivery / SMTP✓ By design — Captured in-memory for inspection — no real messages sent
Bearer-token validity / scope enforcement✓ By design — Any non-empty credential is accepted — no real secrets needed
Rate limiting (429) / quota enforcement✓ By design — Never throttles — local tests run at full speed, zero cost
Real DNS verification of domains⟳ Roadmap — Intentionally unsupported (status flips to pending only)
Real idempotency-key 24h expiry⟳ Roadmap — Simplified (replayed until reset)
Segments / Topics / Templates / Webhooks / Logs REST resources⟳ Roadmap — Not part of the stable resend Node SDK surface audited

Error codes & shapes

Errors use the Resend envelope:

{ "statusCode": 422, "name": "validation_error", "message": "..." }
StatusnameWhen
400invalid_idempotency_keyIdempotency-Key longer than 256 chars
400validation_errormalformed JSON body, invalid recipients, template+content conflict/absence, batch shape/limit, broadcast scheduling/delete rules
401missing_api_keyno Authorization: Bearer header
404not_foundunknown resource id or endpoint
405method_not_allowedmethod not allowed for the path
422missing_required_fieldrequired field (e.g. from, to, subject, name, email) missing
422invalid_from_addressfrom not a valid email@x or Name <email@x>
422invalid_attachmentattachment missing both content and path
422invalid_regiondomain region not one of us-east-1, eu-west-1, sa-east-1, ap-northeast-1
422invalid_accessAPI key permission not full_access/sending_access
500application_errorunexpected server error

The generic validation_error returns 400 (matching the real API's errors reference); only the typed errors above use 422.

The official resend Node SDK does not throw on these; it resolves with { data: null, error: <envelope> }.

Manifest

See services/resend/manifest.json:

<!-- parlel:testenv:start -->

Configuration — test.env

Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.

RESEND_API_KEY=re_parlel_test_key
RESEND_BASE_URL=http://parlel-bridge:4651
<!-- parlel:testenv:end -->