At a glance
Start
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-emptyAuthorization: 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 emailPOST /emails/batch— Send up to 100 emailsGET /emails— List sent emailsGET /emails/:id— Get emailPOST /emails/:id/cancel— Cancel scheduled emailPATCH /emails/:id— Reschedule a scheduled email (scheduled_at)
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
Setresend.allowlist in seed config to test recipient rejection:
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 domainGET /domains— List domainsGET /domains/:id— Get domainPATCH /domains/:id— Update domain: setopen_tracking,click_tracking, andtlsDELETE /domains/:id— Remove domainPOST /domains/:id/verify— Trigger domain verification
API keys
POST /api-keys— Create API key (permission,domain_id)GET /api-keys— List API keysDELETE /api-keys/:id— Delete API key
Audiences and contacts
POST /audiences— Create audienceGET /audiences— List audiencesGET /audiences/:id— Get audienceDELETE /audiences/:id— Delete audience and its contactsPOST,GET /segments,GET,PATCH,DELETE /segments/:id— Segments: the same records as audiencesPOST /audiences/:audience_id/contacts— Add contactGET /audiences/:audience_id/contacts(or/segments/:id/contacts) — List contactsGET /audiences/:audience_id/contacts/:id_or_email— Get contactPATCH /audiences/:audience_id/contacts/:id_or_email— Update contact:first_name,last_name,unsubscribedDELETE /audiences/:audience_id/contacts/:id_or_email— Delete contact
/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 omittedpermission: full_access (default) or sending_accessdomain: 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 emailsGET /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.