Simulators
Resend changelog
What changed in each Resend simulator release, newest first.
v0.0.11 — 2026-11-06
- The docs gather errors and limits in one section. It shows Resend’s flat error body, the shared ten-requests-per-second window and its headers, how
GET /emailsandGET /webhookspage, and how an idempotency key replays a send.
v0.0.10 — 2026-11-06
- The docs name Resend’s base URL option and read a seeded email back by its id. The connect section names
resend.api_urlfor Python, and the seeding example reads the email back withGET /emails/{id}.
v0.0.9 — 2026-11-05
-
Swagger UI to read and call the twin from the browser.
/_rystic/swagger, linked from the state UI header, is Swagger UI over the published API trimmed to the operations this twin serves; Try it out sends each request to the twin. -
The twin documents its own control plane in Swagger.
/_rystic/swaggernow lists a Rystic control plane spec, served at/_rystic/swagger/spec/rystic-control-plane.json, describing the/_rystic/*routes this twin serves (its scenarios, inspect views and seedable registers by name) and its/_bounceroute for bouncing a sent email. -
A state dump seeds back as it was, and the next record gets a fresh id.
GET /_rystic/inspectposted toPOST /_rystic/seedafter a reset restores every record where it was, per-caller records included, which used to land under a doubled key. The next record created after any seed passes every uuid and event id the simulator minted that the seed holds.GET /_rystic/inspect?view=sequenceslists every id counter, and a world document’s newsequenceskey restores them, so ids the simulator renders from a counter continue where the dump left off. -
A slow client no longer holds up the others. The simulator reads a request’s whole body before it starts serving it and sends the answer after, so a client that uploads slowly, or reads a large answer slowly, no longer delays other clients’ requests. Answers are unchanged; one larger than 4 MiB still makes other requests wait while it is sent.
-
The call log shows what each call asked for and whether it succeeded. Each
GET /_rystic/callsentry now carriesargs, the call’s query and body arguments with credentials redacted (long values cut, file contents left out), andok, false for an answer that is not 2xx. A path the simulator does not serve is markedunknown_route: true.?tail=Nreturns only the last N calls, anddroppedcounts the calls the log no longer keeps. -
The docs are one page, with a page per feature. The archive ships
README.mdin place ofHOWTO.md, with the same sections in the same order as every Rystic simulator’s page, andCHANGELOG.mdbeside it. Scheduled emails and the Dashboard each have their own page underdocs/features/. Sandbox recipients and bounces share another page. Every example on these pages runs as written against a fresh simulator. -
The docs page links the admin token section. The page now sends you to the admin token’s section of the shared-simulators page.
-
Scenario descriptions use customer terms. The Scenarios table and
--list-scenariosdescribe each scenario in American spelling, with no internal references. -
The Scenarios table is easier to scan. Each row says in a line what the scenario is for, and a list below the table says what each scenario holds.
-
Pro starts with a 14-day trial. Free has ended, so every simulator is on Pro and Enterprise, and the docs name only those plans.
-
Limitations is shorter to read. It opens with one sentence counting the limitations. The detailed lists follow, and the website folds them behind “Show all limitations”.
-
The page has no Plans section. The Quickstart names the plans that run the simulator, and the pricing page lists what each plan includes.
-
The docs read more plainly, and each difference from Resend is marked. Sentences are shorter, and each behavior that differs from Resend is marked “Differs from Resend” where it’s described, such as an email to
delivered@resend.devthat staysqueued. Rules with several outcomes are tables: idempotency keys, a missing or malformed API key, the bounce route’s refusals and the scheduling window. Auth, seeding and reset keep only the simulator’s own facts, and the shared pages cover the rest.
v0.0.8 — 2026-10-12
-
A clock jump fires each timeline event at its own time. When one
POST /_rystic/clockor paced tick passes several events, each fires with the clock at itsat, so the records it creates carry that time, and the clock then lands where the move asked. An event already due when the clock moves fires at the current time. -
Webhook signatures can follow a shifted receiver clock.
POST /_rystic/clock {"wall_offset_seconds": N}signs every webhook delivery N seconds past the wall clock, so a receiver whose own clock you moved forward (assandbox clock -advancedoes) still accepts thesvix-timestamp;0or/_rystic/resetputs it back. -
A timeline call can carry raw bytes, XML or a JSON array. A
callevent may sendbody_textorbody_base64instead ofbody; the twin sends it as written, under the route’s own content type (XML, bytes, JSON or a form), so a scheduled call can upload an object’s bytes or post an XML document, and${name}fills inbody_text. Abodymay be a JSON array where the route reads one. A call that answers XML binds from its elements by JSON Pointer, such as/InitiateMultipartUploadResult/UploadId.
v0.0.7 — 2026-10-12
-
Registered webhooks can receive their events. Boot with
RYSTIC_WEBHOOK_DELIVERY=onand each webhook event a send triggers is POSTed to every enabled webhook registered throughPOST /webhookswhoseeventsnames it, signed with itssigning_secretin Svix headers at wall-clock time so svix’sWebhook.verifyaccepts it however the simulator clock is pinned; without the variable nothing is POSTed. -
A world can carry its own timeline. A
--seedfile, or a complete world document posted toPOST /_rystic/seed, may addcredentials, binding presented credentials to actors, andevents: calls the twin makes on its own API at set sim times as one of those actors, through the same handlers and egress as your requests. A top-levelcredentialsoreventskey the twin used to ignore now takes effect.RYSTIC_PACE=<speed>plays the clock, and the timeline, in real time, and an event’satmay be+<duration>./_rystic/inject,--injectandRYSTIC_INJECTnow needstate_setinstead ofscenarios. -
Sandbox recipients bounce and complain. An immediate send whose first
torecipient isbounced@resend.devorcomplained@resend.dev, with or without a+label, now emitsemail.bounced, oremail.deliveredandemail.complained, afteremail.sent, and the email readslast_eventbouncedorcomplained; underRYSTIC_WEBHOOK_DELIVERY=oneach is delivered Svix-signed to the webhooks subscribed to it. A bounced email’semail.bouncedandGET /emails/{email_id}carry thebounceobject live Resend sends. -
A timeline call can hand what it minted to later events. A
callevent may addbind,{"<name>": "<JSON Pointer>"}, saving fields of its answer, such as an ID the twin minted; a later event uses one as${name}in a call’s path, headers or body or in a seed event’s record keys and values (a register name cannot be bound). A schedule naming a${name}no earlier call binds is refused at load, and an event whose binding call failed records an error instead of firing. -
design_studiocarries a timeline. As the clock moves past2026-03-01T00:00:00Z, the world registers a billing webhook, sends Copperline Coffee’s March plan invoice, and schedules an overdue reminder to Pickford Law, moves it off a Sunday and cancels it, each a call on the twin’s own API under the world’s bound key. Those calls take the first email and webhook IDs, so a test that advances the clock before sending sees its own email minted later in the sequence. -
A world can bounce an email.
POST /_bouncebounces an email that went out to any address: it readsbouncedwith itsbounce, andemail.bouncedreaches the egress log and every webhook subscribed to it. A world’s timeline schedules it like any other call.
v0.0.6 — 2026-10-11
-
An email and a webhook never share an ID. Each kind of record draws its IDs from its own range: the first email you send is
28c9b732-cf71-466d-8000-000000000001, where it and the first webhook were both00000001-0000-4000-8001-000000000001. Ids still count up in their last digits and restart on reset, and webhook signing secrets change the same way; a test that pinned an old ID needs the new one. -
Every route refuses a missing or malformed key the way Resend does. All ten routes check
Authorizationbefore anything else: no header answers401 missing_api_keywith “Missing API Key”, and an empty Bearer key or another scheme such asBasic abcanswers400 validation_errorwith “API key is invalid”, where the twin used to answer 401 with “Missing API key in the authorization header.”GET /emails,POST /emails/{email_id}/cancel,POST /webhooks,GET /webhooks/{webhook_id}andDELETE /webhooks/{webhook_id}used to answer without a key, so a test that calls them without one now needs a placeholder key. -
A scheduled send no longer shows an SMTP handoff.
POST /emailswithscheduled_atwrites only itsemail.scheduledevent toGET /_rystic/egress, since Resend hands the mail to SMTP only at the scheduled time; an immediate send still writes the handoff beforeemail.sent. -
A send or update with a form Content-Type is refused as Resend refuses it.
POST /emailsandPATCH /emails/{id}with a JSON body labeledapplication/x-www-form-urlencodedanswer 422 with Resend’s message (Missing `to` field.andThe `scheduled_at` field is required.), where the simulator answered 400 to a send and applied the update. Label JSON bodiesapplication/json. -
A scheduled email’s
scheduled_atreads back as Resend renders it.GET /emails/{id}andGET /emailsreturnscheduled_atin the same form ascreated_at, such as2026-01-03 00:00:00.439000+00, where the simulator echoed an ISO 8601 string such as2026-01-03T00:00:00.439Z. A test that compares the string should compare the instant. -
The scripts in this archive are MIT-licensed.
check.shand the archive’s other scripts are published under the MIT License at https://github.com/Rystic-Inc/rystic-scripts , so you can copy, change and share them.LICENSE.txtno longer covers them; it still governs the twin itself.
v0.0.5 — 2026-09-30
-
Scheduled email reads preserve null values. Text-only emails return
html: null, and newly scheduled emails returnmessage_id: nulluntil their state indicates a completed send. Explicit HTML, including an empty string, and existing message identifiers are preserved. -
Resend fidelity limitations are explicit. The HOWTO and website docs disclose missing throttling and sender-domain checks, distinguish local auth errors from measured Resend responses, and explain how to choose scheduled dates without treating
initial_clockas the scenario clock. The dated fidelity result and absence of a standing daily converge round remain explicit. -
Sent-email events include ISO timestamps, message IDs and tags. The
email.sentevents inGET /_rystic/egressuse UTC ISO 8601 timestamps and includedata.message_idand supplied tags as a name/value map, so tests can inspect these fields in the outbound event log. -
Request admission shares a ten-per-second window across credentials, emits rate-limit headers on API responses, and returns 429 with retry-after when exhausted. The window uses real time; control-plane requests are exempt and reset restores the allowance. Daily/monthly quotas remain unmodeled. The full 39-probe live round matched and exercised 36/39, including the header and actual 429 burst probes; the three known POST/PATCH differences remain.
Burst comparison requires a 429 observed from the live API in both two-way and three-way runs; if the live API never refuses, the result is inconclusive. Wire recording emits each burst request once, without duplicating the selected response as a synthetic exchange.
The 36/39 rate-limit acceptance round predates the merge of the webhook fields. Both features and their separate live records are retained; the combined simulator is covered by local regression tests, without claiming a new combined live round.
-
Rate-limit headers arrive spelled as Resend spells them.
ratelimit-limit,ratelimit-remaining,ratelimit-reset,ratelimit-policyandretry-afternow reach the wire lowercase instead of asRatelimit-Limitand friends, so a client that keys on the received header name sees the same keys it sees from Resend. -
The state UI shows the outbox as Resend’s dashboard. On Pro and Enterprise,
/_rystic/uiopens a live Resend-style view of the twin — emails with their status, events and rendered body, and webhooks with the events they would receive — where you can reschedule or cancel a scheduled email and add, edit, disable or delete webhooks through the API, beside Your program and Egress cards.
v0.0.4 — 2026-09-26
- The guide starts from an API key, says what each plan reaches, and documents the outbound mail log.
HOWTO.mdnow opens withrystic login rys_…, says what Free, Pro and Enterprise boot and which control-plane routes each answers, and names the five routes that answer401 missing_api_keywithout a bearer key and the five that check none;SCENARIOS.mdsays what each plan boots. EachPOST /emailswrites the SMTP message and itsemail.sentoremail.scheduledevent toGET /_rystic/egress, and the guide now says so.check.shfinds the API keyrystic loginstored as well as a license file, looked up in the CLI’s order, andrystic featuresmarks the features and commands that need Pro or Enterprise.
v0.0.3 — 2026-09-26
-
The fidelity number is current again. The first full-suite comparison on the 38-probe suite scored 35 of 38 against api.resend.com on 2026-09-25, and the HOWTO and site page name the three misses (content-type mismatch on sending and updating an email, and the non-object send body) with the repair that closes them.
-
Only
design_studioships as a named scenario. The older scenarios are now test-only fixtures and are no longer accepted by--scenario,RYSTIC_SCENARIO, orPOST /_rystic/scenario. Tests and evaluations can load their unchanged register maps and clocks fromsim/testdata/scenarios/through the control plane.
v0.0.2 — 2026-09-25
-
Self-serve on the metered edition replaces the free edition. A Rystic account’s token runs the metered build:
rystic login rys_…once, thenrystic runordocker run -e RYSTIC_TOKEN=…. It carries every scenario and control-plane route your plan includes, and bills active 30-second blocks against the plan’s allowance. The fixed-world free download and its usage report are gone. -
A new world,
design_studio, now the default.--scenario design_studioboots Website Design Contractors’ outbox: 595 emails since November 2024 (invoices, receipts, overdue reminders, a declined-card notice, a refund notice, welcome and launch notes, and one receipt bounced off a departed contact, then resent), for the same clients, invoice numbers and dates the studio’s Slack, Stripe, PayPal and Lob worlds hold. A lease that grants no scenarios boots it instead ofbusy_outbox. -
The limitations now date the fidelity evidence. The HOWTO and the site page say which comparison rounds against api.resend.com stand behind the release: the 25/25 full-suite round of 2026-09-02 and the scoped rounds of 2026-09-24 that verified the 13 probes added since, with no full-suite round yet on the current 38-probe suite. The package manifest also describes the metered edition instead of the retired free edition.
v0.0.1 — 2026-09-24
-
First Resend simulator release: email lifecycle and webhook APIs, three deterministic scenarios, licensed and free archives, and customer quickstart. Includes measured validation and idempotency behavior; asynchronous delivery and documented fidelity limitations remain outside the release claim.
-
Resend email and webhook workflows, locally. The first package provides ten stateful API routes, three embedded scenarios, a smoke check, and a quickstart. Forward email paging now traverses all records. No email or webhook is delivered; scheduling, validation, collection and fixture limitations are documented explicitly.