Skip to main content
Resend email API emulation with email sending, domain management, API keys, audiences, contacts, and a local inbox for captured messages.

At a glance

Start

When all services are started, Resend listens on http://localhost:4008; alone, it starts on http://localhost:4000. Point the resend SDK at it with new Resend(apiKey, { baseUrl: "http://localhost:4008" }).

Auth

Any non-empty Authorization: Bearer token is accepted, and a request with no header falls back to the default user. Keys the emulator issued, from POST /api-keys or the api_keys seed, behave like Resend’s: a deleted key is refused with 403 invalid_api_key, and a sending_access key may only call POST /emails and POST /emails/batch (anything else is 401 restricted_api_key). A sending_access key created with a domain_id, or seeded with a domain, refuses mail from any other domain with 403. GET /api-keys reports each key’s last_used_at. Where tokens are configured with no fallback user, as in startService tests with tokens, an unknown token is 403 invalid_api_key.

Emails

  • POST /emails — Send single email
  • POST /emails/batch — Send up to 100 emails
  • GET /emails — List sent emails
  • GET /emails/:id — Get email
  • POST /emails/:id/cancel — Cancel scheduled email
  • PATCH /emails/:id — Reschedule a scheduled email (scheduled_at)
An email’s last_event is delivered, scheduled, or canceled, as Resend reports it; webhook event types keep the email. prefix. Creating an API key, domain, audience, or segment returns 201.

Idempotency

POST /emails and POST /emails/batch accept the case-insensitive Idempotency-Key header. Keys must be 1 to 256 characters and are retained for 24 hours. Repeating a request with the same key and validated payload returns the original response without creating another email or dispatching duplicate webhooks. Reusing a key with a different payload or endpoint returns 409 invalid_idempotent_request. Invalid key lengths return 400 invalid_idempotency_key. Requests without the header keep the normal non-idempotent behavior.

Recipient restrictions and send attempts

Set resend.allowlist in seed config to test recipient rejection:
Omitting allowlist accepts every recipient; allowlist: [] rejects every recipient. Every address in to, cc, and bcc must be listed. Matching ignores surrounding whitespace and letter case; entries are individual addresses, with no wildcard or domain matching. This is a local testing control, independent of Resend’s domain verification and account restrictions. A rejected send returns 403 validation_error naming the rejected addresses and configured allowlist. No email ID, email record, idempotency record, or webhook is created. A batch is rejected as a whole if any recipient is disallowed. Retrying a rejected request, even with the same idempotency key, creates a new attempt. Replaying an accepted idempotent request returns its original response even if the allowlist has since changed. Validated sends append one record per email to the resend.send_attempts store collection, including emails in a rejected batch. Each record contains from, to, cc, bcc, subject, idempotency_key (or null), and the store’s id, created_at, and updated_at. Message bodies are not copied into attempts. Malformed requests, invalid or conflicting idempotency keys, and idempotency replays do not append attempts. Read attempts with getResendStore(store).sendAttempts.all() from @deploybaseline/resend, or inspect the persisted state. Shared adapter snapshots use the key resend:resend.send_attempts. Attempts and recipient restrictions survive saved-state restoration, including rejected sends. There is no additional Resend API endpoint for attempts. A rejected send has an attempt without an email; a send that never happened has neither.

Domains

  • POST /domains — Create domain
  • GET /domains — List domains
  • GET /domains/:id — Get domain
  • PATCH /domains/:id — Update domain: set open_tracking, click_tracking, and tls
  • DELETE /domains/:id — Remove domain
  • POST /domains/:id/verify — Trigger domain verification

API keys

  • POST /api-keys — Create API key (permission, domain_id)
  • GET /api-keys — List API keys
  • DELETE /api-keys/:id — Delete API key
Issued keys are enforced as described under Auth.

Audiences and contacts

  • POST /audiences — Create audience
  • GET /audiences — List audiences
  • GET /audiences/:id — Get audience
  • DELETE /audiences/:id — Delete audience and its contacts
  • POST, GET /segments, GET, PATCH, DELETE /segments/:id — Segments: the same records as audiences
  • POST /audiences/:audience_id/contacts — Add contact
  • GET /audiences/:audience_id/contacts (or /segments/:id/contacts) — List contacts
  • GET /audiences/:audience_id/contacts/:id_or_email — Get contact
  • PATCH /audiences/:audience_id/contacts/:id_or_email — Update contact: first_name, last_name, unsubscribed
  • DELETE /audiences/:audience_id/contacts/:id_or_email — Delete contact
Resend renamed audiences to segments. /segments serves the same records as /audiences (the resend SDK v6 sends resend.audiences.* calls there), and PATCH /segments/:id renames one. Deleting an audience or segment removes its contacts. A contact under an audience is addressed by its id or its email address.

Seed config

  • token: optional; generated when omitted
  • permission: full_access (default) or sending_access
  • domain: optional; a seeded domain this key may send from
Seeded keys are checked like keys from POST /api-keys: a sending_access key may only send, and one with a domain only from that domain. See Auth.

Inbox

  • GET /inbox — List captured emails
  • GET /inbox/:id — View captured email

Limits

Resend’s global /contacts API, topics, and contact properties are not implemented yet.

Conformance

See CONFORMANCE.md for every provider.