# Stripe changelog

What changed in each Stripe simulator release, newest first.

What changed in each Stripe simulator release, newest first.

## v0.0.19 — 2026-11-05

1. **Long clock moves run faster and hold less memory.** Each event a `/_rystic/clock` move emits is copied fewer times on its way into the event logs, so 1,000 subscriptions advance 24 months in about 16 seconds instead of 22, with about a tenth less memory at the end.

2. **Every body parameter the API reference documents for an endpoint is accepted.** A request naming one the twin used to refuse with `400 parameter_unknown`, such as `preferred_locales[]` on `POST /v1/customers/{customer}`, `expand` on an update, or the 58 payment method type hashes (`acss_debit`, `affirm`, ...) of `POST /v1/payment_methods`, now goes through, and the twin ignores whatever it does not model. `POST /v1/prices`, `POST /v1/subscriptions` and `POST /v1/payment_intents/{intent}/confirm` still refuse some of theirs.

3. **A request with no API key is refused on every route.** As Stripe does, the twin answers `401` `You did not provide an API key.` before it reads the path or body, in v2's envelope (code `api_keys_missing`) on a `/v2/` route; it used to serve such a request on every route but five lists. Send a placeholder `sk_test_...` key on every request.

4. **Every captured charge's balance transaction resolves and expands.** A card charge captured by a PaymentIntent's capture (whole or in part), by `POST /v1/charges` or its capture route, by an invoice's payment or by a subscription's first invoice now writes the `charge` balance transaction its `balance_transaction` names, so `GET /v1/balance_transactions/{id}` answers it and `GET /v1/balance_transactions?source=<charge>` lists it, where the twin answered 404 and listed nothing. `expand[]=balance_transaction` on a charge embeds it, as Stripe does; the twin refused that expansion before.

5. **A value outside a parameter's enum is refused.** As Stripe does, the twin answers `400` `invalid_request_error` naming the parameter (`Invalid tax_exempt: must be one of exempt, none, or reverse`) before it stores or acts on anything, and v2's `invalid_fields` envelope on a `/v2/` route; it used to store `tax_exempt=bogus` or refund with `reason=because`. The message lists the values in the spec's order, which can differ from Stripe's. An empty value still reads as an attempt to unset the parameter, and `origin` on a refund create is now refused as an unknown parameter, as Stripe refuses it.

6. **An invoice carries the fields Stripe sends, and no others.** Every invoice the twin answers (a retrieve, a list or search row, a subscription's embedded `latest_invoice` and an invoice event's object) now carries the 74 fields Stripe returns, among them `account_country`, `account_name`, `customer_email`, `customer_name`, `customer_address`, `customer_tax_exempt`, `total_discount_amounts` and `total_pretax_credit_amounts`, 22 of which a subscription's invoice lacked. It no longer carries a top-level `subscription` (read `parent.subscription_details.subscription`, as on Stripe), `amount_paid_off_stripe`, or, outside `expand[]=payments`, `payments`. `webhooks_delivered_at` reads the invoice's `created`, where a subscription's invoice read null and a finalized one-off invoice read its finalize time.

7. **A PaymentIntent create or confirm refuses a payment method it can't find, and the fraud test cards decline.** `POST /v1/payment_intents` and `POST /v1/payment_intents/{intent}/confirm` naming a payment method that is neither a stored PaymentMethod nor a test card the twin serves, such as `pm_card_doesNotExist` or `pm_doesNotExist`, now answer Stripe's 400 `resource_missing` ("No such PaymentMethod: '…'") and write nothing; the confirm's refusal carries the unchanged intent. The twin used to accept any such id: a confirming create or a confirm answered `succeeded`. A Stripe test card the twin doesn't serve, such as `pm_card_ca`, is refused the same way, as attach and customer create already refuse it; the HOWTO lists the cards it serves. `pm_card_chargeDeclinedFraudulent`, `pm_card_visa_chargeDeclinedFraudulent` and `pm_card_radarBlock` now decline with `card_declined` and `decline_code` `fraudulent` at create with `confirm=true` and on confirm, where they succeeded.

8. **The twin documents its own control plane in Swagger.** `/_rystic/swagger` now 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 `/_dispute` route for raising a dispute on a charge.

9. **A state dump seeds back as it was, and the next record gets a fresh id.** `GET /_rystic/inspect` posted to `POST /_rystic/seed` after 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=sequences` lists every id counter, and a world document's new `sequences` key restores them, so ids the simulator renders from a counter continue where the dump left off.

10. **Coupons, promotion codes and invoice previews.** The simulator serves `/v1/coupons` (create, retrieve, update, delete, list), `/v1/promotion_codes` (create, retrieve, update, list) and `POST /v1/invoices/create_preview`, and `/_rystic/seed` takes `state_coupon` and `state_promotion_code` records.

11. **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.

12. **A one-time tiered price is refused, and invoices redeem their discounts.** Creating a tiered price without `recurring` answers 400 `invalid_request_error`, param `interval`, "Prices with `type=one_time` are not supported with tiered billing.", as Stripe does. Creating an invoice or invoice item whose `discounts` name a promotion code or coupon redeems it: its `times_redeemed`, and the promoted coupon's, grow by one, and once `max_redemptions` is used the code reads `active: false` and the coupon `valid: false`. A later invoice, invoice item or preview that offers a used-up, inactive or unknown code or coupon is refused, and nothing is redeemed.

13. **An invoice preview prices packs, sub-cent prices and line discounts, and the price list matches Stripe.** `POST /v1/invoices/create_preview` bills a price with `transform_quantity` by whole packs, bills a `unit_amount_decimal` price at its exact decimal rounded half up once per line, and applies a line's own `discounts` to that line before the subtotal. `GET /v1/prices` without an `active` filter lists archived prices too. A price's `currency_options` appears only when the request expands it, and then holds the price's own currency beside the others.

14. **The call log shows what each call asked for and whether it succeeded.** Each `GET /_rystic/calls` entry now carries `args`, the call's query and body arguments with credentials redacted (long values cut, file contents left out), and `ok`, false for an answer that is not 2xx. A path the simulator does not serve is marked `unknown_route: true`. `?tail=N` returns only the last N calls, and `dropped` counts the calls the log no longer keeps.

15. **The docs are one page, with a page per feature.** The archive ships `README.md` in place of `HOWTO.md`, with the same sections in the same order as every Rystic simulator's page, and `CHANGELOG.md` beside it. Subscriptions and invoices, test clocks, Connect, test cards and disputes, bank debits, the balance, the hosted pages and the Dashboard each have their own page under `docs/features/`, and every example on these pages runs as written against a fresh simulator.

16. **The docs page links the admin token section.** The page now sends you to the admin token's section of the shared-simulators page.

17. **Scenario descriptions use customer terms.** The Scenarios table and `--list-scenarios` describe each scenario in American spelling, with no internal references.

18. **Placeholders in the docs show on the site.** A placeholder such as `<id>` in the docs now shows on the site, where it was left out.

19. **Draft invoice discounts carry their amounts and expand into Discount objects.** An invoice created with `discounts` answers them as `di_` ids, and `expand[]=discounts` or `expand[]=lines.data.discounts` on an invoice retrieve (`data.discounts` on a list) answers each as a Discount object naming its `promotion_code` and `source.coupon`. On a draft, an invoice-level discount covers only the discountable lines, a fixed amount is capped at what those lines cost, and each line's `discount_amounts` and the invoice's `total_discount_amounts` list every discount's share, recomputed whenever an invoice item is created, updated or deleted, lines are added, pending items are included at creation or the invoice's discounts change. A draft lists its invoice item lines newest first. Reusing a `discount` id that is not the customer's own, or naming one that does not exist, is refused, as Stripe does.

20. **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.

21. **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.

## v0.0.18 — 2026-10-20

1. **A customer base moves through two years in seconds.** A `/_rystic/clock` move settles each stop's subscription renewals, trial ends, invoice finalizations and payment retries in one pass, so 1,000 subscriptions advance 24 months in about 30 seconds instead of several minutes, and reads after the move stay as fast as on an empty world. A world whose subscriptions all fall due at different instants still slows as it grows.

2. **A subscription's automatically paid invoices send `invoice_payment.paid`.** A create that pays its first invoice from a card now sends `invoice_payment.paid` between `invoice.created` and `invoice.paid`, a renewal or trial-end draft the clock finalizes and pays sends it after `invoice.payment_succeeded`, and a retried payment that succeeds, an update's `trial_end=now` and an `always_invoice` item add that pay from the card send it too, each carrying the paid invoice payment (`status` `paid`, `invoice` the invoice's ID, `amount_paid` the amount, `payment` naming the PaymentIntent) as Stripe does. An invoice paid at zero, a trial's or one a credit covers, has no invoice payment and sends none. Before, only an invoice pay and a PaymentIntent confirm sent it, so a billing flow's events missed one type per automatic payment.

3. **A subscription's renewal is announced seven days ahead.** `invoice.upcoming` now goes out seven days before every renewal's period end, naming no request and previewing the renewal invoice with its lines, amount and period; before, it went out only before a trial's end. The lead is an account setting, "upcoming renewal events", which the twin does not model and fixes at the measured account's seven days. A subscription set to cancel at its period end gets none, nor does a period shorter than seven days. A `send_invoice` subscription's renewal and trial-end drafts, the open invoices they become, its `invoice.upcoming` previews and a cancel's final invoice now read `next_payment_attempt` null, as Stripe documents for every `send_invoice` invoice; before, the drafts, their open invoices and the trial-end preview carried the finalize hour.

## v0.0.17 — 2026-10-20

1. **Requests stay fast as subscriptions accumulate.** A request's cost no longer grows with the number of subscriptions in the world: a `GET` on a world of 1,000 subscriptions answers as fast as on an empty one, and creating 1,000 subscriptions and moving `/_rystic/clock` through three months takes seconds instead of minutes.

2. **`pm_card_chargeCustomerFail` is declined on a PaymentIntent, an invoice pay and a subscription create too.** A PaymentIntent created with `confirm=true` or confirmed with the card, and an invoice pay charging it, now answer Stripe's 402 `card_error` (`card_declined`, `generic_decline`) where they used to succeed, leaving a `failed` charge and the PaymentIntent `requires_payment_method`, sent as `charge.failed` and `payment_intent.payment_failed`; the invoice a pay names stays open with one attempt and sends `invoice.payment_failed`. A subscription create charging the card reads `incomplete` with its first invoice open, and under `payment_behavior=error_if_incomplete` it answers the 402, creates no subscription or invoice and cancels its PaymentIntent. A subscription update's `trial_end=now` charging the card is declined the same way, its invoice left open.

3. **A JCB charge reads `electronic_commerce_indicator` null.** `POST /v1/charges` with a JCB test card answers `payment_method_details.card.electronic_commerce_indicator` null, as a PaymentIntent's charge already did, where it used to read `07`.

4. **A refund made through `POST /v1/charges/{charge}/refund` sends four more refund members in its events.** Its `refund.created`, `refund.updated` and `charge.refund.updated` objects carry `customer_account`, `receipt_number`, `source_transfer_reversal` and `transfer_reversal` as null, as Stripe sends them and as a read of the refund already answered, where the events used to leave them out.

5. **Both charge refund routes write the refund's balance transaction.** A refund of a captured charge through `POST /v1/charges/{charge}/refunds` or the legacy `POST /v1/charges/{charge}/refund` names its balance transaction as one through `POST /v1/refunds` does: `GET /v1/balance_transactions/{id}` reads it as type `refund`, `pending`, the refunded amount negated with no fee, and `GET /v1/balance_transactions?source=<refund>` lists it, a partial refund and the refund of the rest each their own. The first route named an ID no balance transaction answered, and the legacy route named none. A release of an uncaptured charge still writes none, and so does a legacy refund of a charge whose own balance transaction has not attached yet, which is `succeeded` at once as Stripe's is.

6. **A Checkout Session create refuses an `expires_at` that is not an integer.** `POST /v1/checkout/sessions` answers an `expires_at` such as `abc`, `1e9`, `1800.0`, an empty value or one with a leading space with Stripe's 400 `parameter_invalid_integer`, naming `param` `expires_at` and quoting the value as sent, and creates nothing, where it used to ignore it and apply the 24-hour default. A seeded session whose `expires_at` is missing, or not a timestamp, now reads, lists and expires a day after its `created`, where each of those used to read it differently.

7. **A credit note create refuses an `effective_at` Stripe refuses.** `POST /v1/credit_notes` answers an `effective_at` that is not an integer, an empty value or one with a leading space included, with Stripe's 400 `parameter_invalid_integer`, one before the invoice was finalized with ``Specified effective date must be after the associated invoice's `finalized_at`.``, and one after the clock with `Specified effective date must be within past 5 years.`, as the preview does, and writes no credit note, where it used to create one with any value it could read. A create with no `invoice` still answers `parameter_missing` first.

8. **A credit note preview refuses an empty `invoice`, and a missing invoice is refused before `effective_at`.** `GET /v1/credit_notes/preview` answers `invoice=` with Stripe's 400 `parameter_invalid_empty` on `invoice`, where it used to answer `parameter_missing`. A preview or a create naming an invoice that does not exist answers `resource_missing` on `invoice` whatever its `effective_at`, as Stripe's preview does, where an `effective_at` that is not an integer or in the future used to be refused first.

9. **A credit note preview refuses an `effective_at` Stripe refuses.** `GET /v1/credit_notes/preview` answers an `effective_at` that is not an integer, an empty value or one with a leading space included, with Stripe's 400 `parameter_invalid_integer`, one before the invoice was finalized, however far back, with ``Specified effective date must be after the associated invoice's `finalized_at`.``, and one after the clock, however far ahead, with `Specified effective date must be within past 5 years.`, where it used to preview any value it could read. A preview with no `invoice` still answers `parameter_missing` first.

10. **A credit note's refund names a balance transaction you can read.** A credit note with a `refund_amount` now writes the balance transaction its refund names, so `GET /v1/balance_transactions/{id}` and `?source=<refund>` answer it: type `refund`, `pending`, the refunded amount negated with no fee, and described `REFUND FOR CHARGE (Payment for Invoice)` for an invoice payment's charge, as Stripe's is. The refund reads `reason` null, as Stripe's does, and its acquirer reference no longer repeats one a refund made through another route holds.

11. **A customer's balance is applied to its next invoice.** A draft invoice now reads `starting_balance` as its customer's `balance` and `amount_due` reduced by it, never below 0: after a -500 credit, a draft with a 1200 item reads `amount_due` 700 with `total` 1200 and `ending_balance` null. Finalizing reads the balance again, sets `ending_balance` (0 once a credit is spent), asks the PaymentIntent for the reduced amount, moves the customer's `balance` to the invoice's `ending_balance` and sends `customer.updated` before the finalize's `invoice.updated`, as Stripe does. A credit that covers the whole invoice leaves it paid at the finalize with no PaymentIntent, and a positive balance is added to the amount due. A subscription's first invoice and its renewals spend the balance the same way, whether a customer update, a credit note or an earlier invoice left it. Before, every invoice read `starting_balance` 0 and the balance was never spent. The `applied_to_invoice` customer balance transaction is not modeled.

12. **A customer created with `payment_method` has that card attached.** `POST /v1/customers` with `payment_method=pm_card_visa`, or a stored `pm_` ID, attaches the PaymentMethod to the new customer as `POST /v1/payment_methods/{id}/attach` does: it lists under `GET /v1/customers/{id}/payment_methods` and `GET /v1/payment_methods?customer=…&type=card`, `invoice_settings[default_payment_method]` naming the same card stores the attached card's ID, and the create sends `payment_method.attached` then `customer.created`. An unknown `payment_method` answers Stripe's 400 `resource_missing` on that parameter and creates no customer; an empty one answers 400 `parameter_invalid_empty` and a hash or an array 400 "Invalid string: …", as the same forms of `test_clock` do (inferred from those, unmeasured); a declining test token such as `pm_card_chargeDeclined` answers the attach route's 402 `card_error`, and a detached card or one another customer holds its 400 `invalid_request_error`, each creating nothing (inferred from the attach route). Before, the parameter was dropped silently and the customer had no card, so Stripe's test-clock recipe stopped at its third step.

13. **A customer create refuses an empty or non-string `test_clock`.** `POST /v1/customers` with `test_clock=` answers Stripe's 400 `parameter_invalid_empty` on `test_clock`, and one with a hash such as `test_clock[x]=y` answers 400 "Invalid string: {"x":"y"}" on `test_clock`; neither creates a customer. Before, both created one.

14. **Deleting a customer leaves its subscriptions' draft invoices uncollectible.** `DELETE /v1/customers/{customer}` now marks each draft invoice of the subscriptions it cancels `uncollectible`, never finalized or charged, sending `invoice.updated` and `invoice.marked_uncollectible` before each `customer.subscription.deleted`, as Stripe does; its pending invoice items stay pending. Before, the clock finalized such a draft an hour later and could charge the card the delete had detached. A void of such an invoice, of any draft, or of an invoice already void, now answers Stripe's message "You can only pass in open invoices. This invoice isn't open.", and a void of an invoice paid with money answers Stripe's "Invoices with `paid` payments cannot be voided."

15. **Paying an incomplete subscription's invoice makes it active, and a declined `trial_end=now` leaves it past due.** Paying the open first invoice of an `incomplete` subscription, with `POST /v1/invoices/{invoice}/pay` or by confirming its PaymentIntent, now makes the subscription `active` at once, as Stripe does, and sends `invoice_payment.paid` and `customer.subscription.updated`. A card pay's events now run `charge.succeeded`, `payment_intent.succeeded`, `invoice_payment.paid`, `invoice.updated`, `invoice.paid`, `invoice.payment_succeeded`, and a pay after a failed attempt no longer counts another attempt. An update's `trial_end=now` whose charge is declined now leaves the subscription `past_due` instead of `active`. A declined confirm stores the PaymentMethod it names without attaching it, `payment_method_configuration_details` names the account's configuration whenever `automatic_payment_methods` is set, and `POST /v1/charges` declines `tok_chargeCustomerFail` and refuses a `pm_` source with Stripe's 400.

16. **A void or uncollectible mark of an invoice that is not open answers Stripe's not-open message.** `POST /v1/invoices/{invoice}/mark_uncollectible` of an invoice that is not `open` now answers Stripe's 400 "You can only pass in open invoices. This invoice isn't open." A void of a `paid` invoice whose `amount_paid` is 0, such as a trial's 0.00 first invoice, answers the same message; only an invoice paid with money, by card or out of band, gets "Invoices with `paid` payments cannot be voided." Before, `mark_uncollectible` answered a message Stripe never sends, and a void of a paid 0.00 invoice answered the paid-invoice one.

17. **An out-of-band pay records its own invoice payment, and a paused update checks its booleans first.** After `POST /v1/invoices/{invoice}/pay` with `paid_out_of_band=true`, `GET /v1/invoice_payments?invoice=` lists a new `paid` invoice payment of `type` `payment_record`, naming a `pr_test_` payment record and no PaymentIntent, and the finalize's invoice payment reads `canceled`, still naming its PaymentIntent. Neither is the default. Before, the finalize's stayed `open` and the default, and nothing else was written. `POST /v1/subscriptions/{id}` with a `cancel_at_period_end` that is neither `true` nor `false` answers Stripe's 400 "Invalid boolean: `<value>`" on `param` `cancel_at_period_end`, even on a `paused` subscription, where it used to answer the paused refusal. With `billing_cycle_anchor=now` beside `cancel_at_period_end`, a `paused` subscription answers the anchor's refusal.

18. **Paying an invoice out of band attempts nothing and cancels its PaymentIntent.** `POST /v1/invoices/{invoice}/pay` with `paid_out_of_band=true` leaves `attempted` and `attempt_count` as they were and cancels the PaymentIntent the finalize created, with `cancellation_reason` `duplicate`. It sends `invoice.updated`, `invoice.paid` and `payment_intent.canceled`, in that order, and no `invoice.payment_succeeded`. Every pay's `invoice.updated` now names `amount_remaining` in `previous_attributes`, and a card payment's names `attempt_count` too. Before, an out-of-band pay counted as an attempt, sent `invoice.payment_succeeded` and left the PaymentIntent waiting for a payment method.

19. **Microdeposit verification takes Stripe's test values in any case, refuses neither or both inputs and fails a SetupIntent, and a verified ACH payment carries Stripe's fee.** `POST /v1/payment_intents/{intent}/verify_microdeposits` and `POST /v1/setup_intents/{intent}/verify_microdeposits` match the test codes in any case and the amounts in either order, and `amounts` 10 and 11 exceed the attempts and 40 and 41 time out, as `SM33CC` and `SM44DD` do. A verify sending neither input answers Stripe's 400 `parameter_missing` on `descriptor_code`, and one sending both 400 `parameters_exclusive`. On a SetupIntent, `SM33CC` answers the attempts-exceeded 400 and `SM44DD` 200 with the SetupIntent `requires_payment_method` and `last_setup_error` `setup_intent_setup_attempt_expired`, each sending `setup_intent.setup_failed` and leaving the PaymentMethod unattached, and a wrong code answers the mismatch 400 with the attempts left, ten per SetupIntent. A verified `us_bank_account` payment's balance transaction reads type `payment` with Stripe's 0.8% ACH fee (8 on 10.00), `available` 5 s after it is created, and its net lands in the balance's `available`. Before, a PaymentIntent verify matched the codes only in capitals, a SetupIntent's wrong or failing verify answered 400 `payment_method_microdeposit_verification_amounts_invalid`, and the ACH balance transaction read type `charge` with the card fee, `pending`, and the balance took the card fee's net into `pending`.

20. **A seller frozen before its activation activates after a re-request.** A connected seller whose merchant configuration is deactivated and re-applied before `card_payments` ever activated reads `card_payments` `null` until it re-requests it; the re-request reads `card_payments` `pending` again, with `stripe_balance.payouts` `pending` when the seller holds a bank account, and both read `active` once `/_rystic/clock` is 85 seconds past the seller's `created`, so the seller charges. The twin used to leave the re-requested `card_payments` `pending` and refuse the charge.

21. **A pending refund settles when the clock reaches it.** A refund of a captured `pm_card_pendingRefund` / `tok_pendingRefund` charge, through `POST /v1/refunds`, either charge refund route or a credit note's `refund_amount`, answers `pending` with `pending_reason` `insufficient_funds` and a card destination that names no acquirer reference yet, as Stripe's does, and its `pending` balance transaction can be retrieved at once. Once `/_rystic/clock` reaches 122 seconds after the refund's `created` it reads `succeeded` without `pending_reason`, and at 249 seconds its acquirer reference lands; each move sends `refund.updated` and `charge.refund.updated` naming no request, stamped at that instant even when one clock move jumps past it, and a connected account's refund sends them as that account's, listed by its `GET /v1/events` and not the platform's. It no longer reads `pending` forever. Every refund read and update, the charge's refund list included, answers `pending_reason` while the refund is pending, and the charge's refund list now answers each refund's `balance_transaction` and `destination_details` as the other refund reads do.

22. **Products and prices announce their creates, updates and deletes.** `POST /v1/products` sends `product.created`, carrying the product as the request answers it, and a create with `default_price_data` now mints that price (named in `default_price`) and sends `plan.created` (for a recurring price) and `price.created` for it first. `POST /v1/prices` sends `plan.created` for a recurring price, carrying the legacy Plan view of the price under the same ID, then `price.created`; a one-time price sends `price.created` alone. So a catalog synced from webhooks hears about new products and prices. `POST /v1/products/{id}` sends `product.updated` and `POST /v1/prices/{price}` sends `plan.updated` (for a recurring price) then `price.updated`, each with `previous_attributes` over the fields the update changed, and `DELETE /v1/products/{id}` sends `product.deleted`. `GET /v2/core/events?object_id=` lists them as thin events. Not yet modeled: Stripe's refusals of a malformed `default_price_data`, and the `product.created` a price create with `product_data` sends. Before, none of these routes wrote an event, and `default_price_data` was ignored.

23. **A `sepa_debit` payment processes, then settles on the clock, and a `us_bank_account` one waits on microdeposits.** `POST /v1/payment_methods` takes `type=sepa_debit` with an `iban`. A PaymentIntent created with `confirm=true` and a `sepa_debit` method answers `processing` with a `pending` charge, and 15 s after the charge the clock settles it, sending `charge.updated`, `payment_intent.succeeded` and `charge.succeeded` as Stripe does, or, for the failing test IBAN `DE62370400440532013001`, returning the intent to `requires_payment_method` with `payment_intent.payment_failed`, `charge.failed` and `charge.updated`; a payment made acting as a connected account settles in that account, and its settle events are the account's. A `us_bank_account` create-and-confirm without Financial Connections answers `requires_action` with `verify_with_microdeposits`, and a `sepa_debit` SetupIntent created with `confirm=true` succeeds at once; before, the PaymentMethod create refused `sepa_debit` and each of these creates answered `requires_payment_method`.

24. **`billing_cycle_anchor=now` restarts the billing period.** An update with `billing_cycle_anchor=now` on an `active` subscription now restarts each item's period at the update, writes no invoice and, unless `proration_behavior` is `none`, leaves one pending proration for the days the new period adds past the old one, which the next renewal bills first, as Stripe does. Before, it moved the anchor and left the current period running.

25. **A trial with no payment method mints a pending SetupIntent.** A `charge_automatically` trialing `POST /v1/subscriptions` whose customer has no default payment method now names a SetupIntent in `pending_setup_intent`, `requires_payment_method` with `usage` `off_session`, and sends `setup_intent.created` before its other events; a customer with no currency takes the subscription's, with `customer.updated`. A `send_invoice` trial, which nobody has measured, keeps `pending_setup_intent` null. A create judges `trial_end` on its customer's test clock, as it stamps the subscription: one at or before that clock answers Stripe's 400 with `param` `trial_end`, and `trial_end` beside `trial_period_days` answers 400 "Received both trial_end and trial_period_days parameters. Please pass in only one." Before, `pending_setup_intent` was null, a past `trial_end` started no trial, and the payment-method check read the twin's clock.

26. **The clock's `payment_intent.created` carries the PaymentIntent as created.** When the clock finalizes a subscription invoice and charges it, `payment_intent.created` reads the PaymentIntent before the charge, `requires_payment_method` with `amount_received` 0 and no `payment_method`, `latest_charge` or `last_payment_error`, as Stripe sends it, where it used to carry the PaymentIntent after the charge.

27. **A subscription create refuses an empty `collection_method` and takes the `cancel_at` literals.** `POST /v1/subscriptions` with `collection_method` empty answers Stripe's 400 `parameter_invalid_empty` naming `collection_method`, before every other check, and writes nothing. Before, it read as omitted and created the subscription. A `cancel_at` of `min_period_end`, `max_period_end` or `max_billed_until` sets `cancel_at` to the first period's end, records the cancellation request and bills that period whole, as Stripe does.

28. **A subscription create refuses a malformed `collection_method`, `trial_end` or `cancel_at`.** Before it looks anything up, `POST /v1/subscriptions` answers Stripe's 400 naming the parameter, checked in this order: a non-empty `collection_method` other than `charge_automatically` or `send_invoice` ("Invalid collection_method: must be one of send_invoice or charge_automatically"), a `trial_end` that is neither `now` nor a unix time, such as `abc`, `1.5` or an RFC 3339 time ("Invalid trial_end: must be one of now"), and a `cancel_at` that is neither a unix time nor `min_period_end`, `max_period_end` or `max_billed_until` ("Invalid cancel_at: must be one of min_period_end, max_period_end, or max_billed_until"); an empty `trial_end` or `cancel_at` answers `parameter_invalid_empty`, an empty `cancel_at` even beside `cancel_at_period_end=true`. Before, these creates answered 200, ignoring the value or reading an RFC 3339 time as a timestamp.

29. **A subscription create takes `cancel_at`.** `POST /v1/subscriptions` now accepts `cancel_at`, recording a cancellation request: `canceled_at` is the create's time and `cancellation_details.reason` is `cancellation_requested`. A `cancel_at` before the first period's end ends that period there, billed on the first invoice as a proration with its own invoice item; under a trial, `trial_end` and the anchor stay and the zero line runs to `cancel_at`. The clock then cancels the subscription at `cancel_at` with no further invoice. A `cancel_at` at or before the create's clock (a test clock's `frozen_time` when the customer is on one) answers Stripe's 400 "The `cancel_at` param can only be set to a date in the future.", and one beside `cancel_at_period_end=true` answers 400 "Received both cancel_at_period_end and cancel_at parameters. Please pass in only one." Before, the create refused `cancel_at` with `parameter_unknown`.

30. **`pm_card_chargeCustomerFail` attaches, and a subscription charging it on the clock goes `past_due`.** Attaching the test card that Stripe attaches but declines at every charge answers 200 with a Visa ending in 0341, where it used to answer 404. When the clock finalizes a subscription invoice that charges it, at a trial's end or a renewal, the charge is declined: it reads `failed` with `card_declined`, the PaymentIntent reads `requires_payment_method` with the decline as its `last_payment_error`, `charge.failed` and `payment_intent.payment_failed` go out, and the invoice stays open with one attempt and its retry dated, the subscription reads `past_due` and the customer `delinquent`; each retry the clock runs on that card is declined the same way. Paying the invoice with another card succeeds its PaymentIntent and clears that `last_payment_error`.

31. **A failed renewal is retried, closing or paying its invoice ends `past_due`, and item changes bill or credit as Stripe does.** When a renewal's payment fails, the open invoice's `next_payment_attempt` reads 42.1 h after the attempt, which puts the ninth attempt where Stripe's fell, and a second `invoice.updated` announces it before `invoice.payment_failed`. The clock then runs that retry: with no card it fails again, `attempt_count` 2 and the next retry 42.1 h on, and the ninth attempt cancels the subscription for `payment_failed`, leaving the invoice open and the customer `delinquent`; with a default card that charges it pays the invoice, `customer.updated` last. Voiding the invoice, marking it uncollectible or paying it returns the `past_due` subscription to `active` in that request, and each clears the customer's `delinquent` with `customer.updated`, even while another of its subscriptions is still `past_due`; an invoice paid any other way does the same at the clock's next settle. Ending a subscription, by `DELETE`, at its `cancel_at` or by deleting its customer, stops automatic collection of the customer's open invoices and of the subscription's renewal draft: each reads `auto_advance` false and no retry, with `invoice.updated` naming no request, and the draft never finalizes; deleting the customer or its test clock marks the draft `uncollectible` instead. `DELETE` with `invoice_now=true&prorate=true`, and a `cancel_at` set inside the period, which cuts the period there, credit the unused time on a final 0.00 invoice paid an hour later onto the customer's `balance`, clearing `delinquent`. A trial shorter than three days sends no `invoice.upcoming`. A paid invoice reads `auto_advance` false and no retry and keeps its `attempt_count`, and its `invoice.updated` holds Stripe's six changes; the pay's events follow Stripe's order, `customer.updated` first and `customer.subscription.updated` last. `POST /v1/subscription_items` under `proration_behavior=always_invoice` bills the added item's proration at once on a `subscription_update` invoice: paid from the card under `charge_automatically`, paid with no payment when it is 0, and a draft that finalizes an hour later under `send_invoice`; under `charge_automatically` with something to bill and no stored payment method the add is refused with Stripe's 400 `resource_missing`. Deleting an item mid-period leaves a pending credit for its unused time, figured from what was last billed for it (339 for 21 days credits -178 for the last 11), keeps any pending proration it had, and sends `invoiceitem.created` and `customer.subscription.updated`. Proration descriptions write a one-digit day as `07 Nov 2026`. Before, `next_payment_attempt` read null and nothing retried, only a paid invoice ended `past_due` and only on the next request, retries never ended, a cancel left its customer's invoices and its renewal draft with `auto_advance` true, `invoice_now` and `prorate` billed nothing, `always_invoice` waited for the renewal, and a deleted item dropped its pending proration, credited nothing and sent no event.

32. **`expand[]=latest_invoice` on a subscription embeds the invoice.** `POST /v1/subscriptions`, `POST /v1/subscriptions/{id}`, `GET /v1/subscriptions/{id}` and `GET /v1/subscriptions` now embed `latest_invoice`, `customer`, `default_payment_method` and `latest_invoice.customer` (with `data.` on the list) under `expand[]` or `expand[0]`, as Stripe does; `items` and `plan` come back embedded either way. A path the subscription cannot expand, such as `latest_invoice.nonexistent`, answers Stripe's 400 `This property cannot be expanded`, and a list path without `data.` answers the 400 that suggests it. A refused create stores nothing. Before, these routes ignored `expand` and answered the invoice ID.

33. **A subscription's `payment_intent.created` carries the PaymentIntent as it was created.** When a subscription create pays its first invoice, or an update's `trial_end=now` pays the new period, `payment_intent.created` reads the PaymentIntent `requires_payment_method` with `amount_received` 0 and a null `payment_method`, `latest_charge` and `last_payment_error`, as Stripe sends it before the charge. Before, it read the PaymentIntent already `succeeded`.

34. **An unpaid resume invoice is voided when its pending update expires, and `customer.subscription.resumed` reads as Stripe's does.** Once the clock passes `pending_update.expires_at`, 23 hours after a resume with `billing_cycle_anchor=now`, the open resume invoice is voided, its PaymentIntent canceled and `pending_update` cleared, and the subscription stays `paused`, sending `customer.subscription.pending_update_expired`; a `DELETE` of the subscription before then voids it the same way first. When the resume invoice is paid, `customer.subscription.resumed` now carries the subscription as it stood before the pending update applied, `trialing`, as Stripe's does, where it read `active`. Before, the invoice stayed open and a later payment still resumed the subscription.

35. **A connected account's subscription renews as that account.** A subscription created acting as a connected account (`Stripe-Account` or `Stripe-Context`) settles on the clock as that account: the PaymentIntent and charge paying its renewal or trial-end invoice are listed by the account's `GET /v1/payment_intents` and `GET /v1/charges`, not the platform's, the payment credits the account's balance, and every event its clock sends (the renewal, the trial warning and end, a failed payment, the return to `active` once its invoice is paid, the resume, a resume's expiry and the cancel at `cancel_at`) is the account's, as an event its own requests cause is. Before, the renewal's payment and events went to the platform.

36. **A trial's end bills only up to `cancel_at`.** When a trial ends on the clock and the subscription's `cancel_at` falls inside the period that starts there, each item's period now ends at `cancel_at` and the trial end's invoice bills one `Time on` proration for those days, as Stripe does and as a renewal already did. Before, the trial's end billed the whole period although the subscription was canceled at `cancel_at` a few days later.

37. **A subscription update refuses a malformed `trial_end` and an empty item ID.** `POST /v1/subscriptions/{id}` with a `trial_end` that is neither `now` nor a unix time, such as `abc`, `1.5` or an RFC 3339 time, answers Stripe's 400 "Invalid trial_end: must be one of now" on `param` `trial_end`. A hash or an array gets the same answer, though Stripe's own answer to one is unmeasured. An update with `items[i][id]` empty answers 400 `parameter_invalid_empty` naming `items[i][id]`. Neither changes the subscription. Before, both answered 200.

38. **A subscription update refuses a past `cancel_at`, and `cancel_at_period_end` or `billing_cycle_anchor=now` on a paused subscription.** `POST /v1/subscriptions/{id}` with a `cancel_at` at or before the subscription's clock, a test clock's `frozen_time` when it is on one, answers Stripe's 400 "The `cancel_at` param can only be set to a date in the future." On a `paused` subscription, `cancel_at_period_end`, true or false, and `billing_cycle_anchor=now` each answer Stripe's 400 asking you to resume it first, writing nothing and sending no event. Before, each was accepted, and a past `cancel_at` or `cancel_at_period_end=true` had the clock cancel the subscription at its next move.

39. **A test clock advance checks its time as Stripe does.** `POST /v1/test_helpers/test_clocks/{id}/advance` with a `frozen_time` before 2000-01-01 or from 2100-01-01 answers Stripe's 400 "The provided timestamp `<t>` is invalid. ..."; one before the clock's time answers "Test clocks can only be advanced forward in time. Requested time `<target>` is before test clock time `<frozen_time>`."; the clock's own time is accepted and completes there; and one past the cap answers Stripe's message naming it. The cap is two of the shortest recurring interval of the live subscriptions on the clock (a `canceled` or `incomplete_expired` one counts for nothing), two calendar months for a monthly one, and six years on a clock holding none; the cap itself is accepted. Before, the twin refused the clock's own time and anything past two years with "frozen_time must be after the test clock's current frozen time."

40. **A test clock advance refuses a time that is not an integer as sent.** `POST /v1/test_helpers/test_clocks/{id}/advance` with a `frozen_time` given as an RFC 3339 time, a decimal such as `1.5`, a space-prefixed or empty value, an array (`frozen_time[]`) or a hash (`frozen_time[a]`) answers Stripe's 400 `parameter_invalid_integer` on `frozen_time`, "Invalid integer: `<value>`", the value echoed as Stripe renders it, and leaves the clock as it was; `+<t>` advances to `<t>`. A clock still advancing answers its 429 before the time is read, whatever the advance sends. Before, the twin advanced to an RFC 3339 or a space-prefixed time and refused a decimal or an empty value as a time not after the clock's.

41. **A test clock create checks its `frozen_time` as Stripe does.** `POST /v1/test_helpers/test_clocks` with a `frozen_time` that is not an integer as sent, such as `abc`, `1.5`, an RFC 3339 time, a space-prefixed or empty value, an array or a hash, answers 400 `parameter_invalid_integer` on `frozen_time`, the value echoed as Stripe renders it (`{:a=>"<t>"}` for `frozen_time[a]`); a negative one answers 400 "Invalid timestamp: must be an integer"; and one before 2000-01-01 or from 2100-01-01 answers Stripe's 400 range refusal. `+<t>` and `0<t>` create at `<t>`. Before, the twin created a clock at an RFC 3339 time or in any year and answered a decimal as a missing parameter.

42. **Test clocks announce their create, advance and completion.** `POST /v1/test_helpers/test_clocks` sends `test_helpers.test_clock.created`, carrying the clock as created. An advance sends `test_helpers.test_clock.advancing` at its request, carrying the clock as the advance answers it, and once it completes, every event the completion sends, then `test_helpers.test_clock.ready`, carrying the clock ready at the new time and naming no request, as Stripe orders them.

43. **Deleting a test clock sends `test_helpers.test_clock.deleted`.** `DELETE /v1/test_helpers/test_clocks/{id}` sends the event, carrying the clock as it stood, after the `customer.deleted` of each customer on the clock, oldest first, and before the `customer.subscription.deleted` of each subscription it cancels, newest first, as Stripe orders them.

44. **A missing test clock answers Stripe's message.** A read, an advance or a delete of a test clock that never existed, or of one already deleted, answers 404 `resource_missing` with Stripe's message, "No such billingclock: '`<id>`'", where it said "No such test clock".

45. **A verified `us_bank_account` payment settles on the clock, and the microdeposit verify answers Stripe's test values.** `POST /v1/payment_intents/{intent}/verify_microdeposits` with `descriptor_code=SM11AA`, or `amounts` 32 and 45, answers the intent `processing` with a `pending` charge, sending `payment_intent.processing` and `charge.pending`, and 20 s later the clock settles it, sending `charge.updated`, `payment_intent.succeeded` and `charge.succeeded` as Stripe does. `SM33CC` answers Stripe's 400 `payment_method_microdeposit_verification_attempts_exceeded` and `SM44DD` answers the intent timed out, each failing the intent with a failed charge, `payment_intent.payment_failed` and `charge.failed`. Any other code or amounts answers Stripe's 400 `payment_method_microdeposit_verification_descriptor_code_mismatch` with the attempts left, and the tenth fails the intent. A `us_bank_account` SetupIntent created with `confirm=true` now awaits microdeposits, and its verify succeeds at once and attaches the PaymentMethod to the customer. A `us_bank_account` PaymentMethod under Stripe's test routing number `110000000` reads `bank_name` `STRIPE TEST BANK` and `status_details` `{}`, and the test account `000123456789` Stripe's fingerprint. Before, the verify accepted any `SM` code, answered `processing` with no charge and no event, and nothing moved the intent after.

## v0.0.16 — 2026-10-19

1. **A balance with no funds reads zeros.** `GET /v1/balance` for an account holding no funds answers one `usd` entry at zero in `available`, `pending` and `refund_and_dispute_prefunding`, where a fresh connected account answered empty lists. A connected account's balance also lists `instant_available` beside `available`, and the platform's lists `connect_reserved` at zero, as Stripe's do.

2. **Each account reads only its own balance transactions.** Under `Stripe-Account` or `Stripe-Context`, `GET /v1/balance_transactions` lists only that connected account's transactions, and with neither header only the platform's, where every account's used to list together. `GET /v1/balance_transactions/{id}` naming another account's transaction answers 404 `resource_missing` "No such balance transaction", as Stripe's does, where it answered 200. A destination charge writes the connected account's own `payment` transaction. Its source is a `py_` payment and its amount the transferred amount. Its fee is the `application_fee_amount`, with one `application_fee` line in `fee_details`. The account lists and retrieves it under its acting header.

3. **A Checkout Session create refuses an `expires_at` outside 30 minutes to 24 hours ahead.** `POST /v1/checkout/sessions` takes an `expires_at` from 30 minutes to 24 hours after the twin clock, both ends included, and answers any other integer with Stripe's 400 `invalid_request_error`: a past value names `param` `expires_at` and quotes the value, a nearer or later one carries no `param`. No session is created. An omitted `expires_at` still defaults to 24 hours after the create.

4. **A Checkout Session expires when the clock reaches its `expires_at`.** Once `/_rystic/clock` reaches an open session's `expires_at` (24 hours after its create unless the create set it), the session reads `expired` with a null `url`, the expire endpoint refuses it as not open, and `checkout.session.expired` goes out naming no request, its `created` at the session's `expires_at` even when one clock move jumps past it, as Stripe expires a session nobody completed; it no longer reads `open` past its deadline.

5. **A PaymentIntent or SetupIntent created without payment method types needs a `return_url` at confirm.** A create that names neither `payment_method_types` nor `automatic_payment_methods` stores and answers `automatic_payment_methods` `{enabled: true, allow_redirects: "always"}`, as one sending only `automatic_payment_methods[enabled]=true` does, and its `/confirm` without a `return_url` answers 400 `invalid_request_error` with Stripe's return_url message, the intent embedded and no `code` or `param` key. The same confirm with a `return_url` succeeds, as does any confirm of an intent created with `payment_method_types` or with `allow_redirects` `never`. A SetupIntent's `automatic_payment_methods.enabled` reads as a boolean.

6. **A PaymentIntent created with `confirm=true` announces its charge before the settle.** Its `charge.succeeded` carries `balance_transaction` null, as a confirm through `/confirm` does, and the balance transaction arrives with the one `charge.updated` about 3 s later on the twin's clock, which a refund within 2.5 s of the payment drops.

7. **A credit note preview carries its `effective_at`.** `GET /v1/credit_notes/preview` answers `effective_at` as the preview's `created` timestamp, or as the `effective_at` the request sends, as Stripe does, where it used to answer `null` unless the request set one.

8. **A v2 account with the customer configuration pays as `customer_account`.** A PaymentIntent update, a SetupIntent update and a PaymentMethod attach take `customer_account` as the PaymentIntent create does, and each answers it beside the `cus_` ID of the account's v1 customer, as do a refund of such a PaymentIntent and `GET /v1/payment_intents?customer_account=`. An unknown account answers 400 `resource_missing`. `GET /v1/customers/{account id}` answers that v1 customer under its own `cus_` ID.

9. **`customer_account` is refused as Stripe refuses it.** Every operation that takes `customer_account` answers an unknown or closed account with 400 `resource_missing` "No such customer: '`<id>`'" on `param` `customer_account`, and a subscription create answers it 404. The PaymentIntent and SetupIntent updates and the PaymentMethod attach also answer 400 for an account without the customer configuration and for `customer` sent beside `customer_account`, and the intents' errors carry the unchanged intent. The `cus_` ID of an account's v1 customer reads back with `customer_account` set, and a PaymentIntent created for it carries `customer_account`. Acting as an account, a recipient configuration counts toward the configuration check only when it requested `stripe_transfers`, and the check comes before parameter validation.

10. **A customer create refuses a test clock that does not exist.** `POST /v1/customers` with a `test_clock` that never existed, or one already deleted, answers Stripe's 400 `resource_missing` on `test_clock`, "No such billingclock: '`<id>`'", and creates no customer. Before, it created the customer on the twin's clock.

11. **Deleting a customer cancels its subscriptions.** `DELETE /v1/customers/{customer}` cancels each of the customer's live subscriptions at once, as Stripe does: each reads `canceled` with `ended_at` at the delete, `canceled_at` there too unless a pending cancellation request keeps its own, and `cancellation_details.reason` `cancellation_requested`, and the clock no longer renews and bills it. `customer.deleted` now carries the customer as it stood, not the deleted stub, and goes out before each `customer.subscription.deleted`; deleting a test clock sends them in that order too.

12. **A declined charge is stored, listed and sent as `charge.failed`.** A `POST /v1/charges` that a declining test token refuses still answers 402. The charge its `error.charge` names can now be retrieved and is listed, reading `status: failed` with Stripe's `failure_code`, `failure_message`, `outcome` and the test card's `last4` and `funding`. A declined charge, and one a Radar block list refuses, now emits `charge.failed` to your endpoints and the event logs.

13. **A destination charge moves both balances.** A PaymentIntent with `transfer_data[destination]` credits the platform the charge's net, as any card payment does, then transfers `transfer_data[amount]` to the connected account when it is set, else the whole amount, and takes any `application_fee_amount` back from it. The connected account's balance, which `GET /v1/balance` reads under `Stripe-Account` or `Stripe-Context`, gains the transfer less the fee. The platform's moves by the net less the transfer plus the fee. Sending both `transfer_data[amount]` and `application_fee_amount` answers 400 `parameters_exclusive`, as Stripe does. The twin moves both balances at the charge; Stripe shows them seconds later.

14. **An invoice can name its payer by `customer_account` alone.** `POST /v1/invoices` with `customer_account` and no `customer` checks only the account, so an unknown or closed one answers 400 `resource_missing` with param `customer_account` and the account ID, where it answered a missing customer.

15. **A PaymentIntent with automatic payment methods lists Link.** A create with `automatic_payment_methods[enabled]=true` and no `payment_method_types` answers `payment_method_types` `["card", "link"]` and a `link` entry in `payment_method_options`, as Stripe's does, where it answered an empty list.

16. **A payout below 1 is refused with Stripe's message.** `POST /v1/payouts` with an `amount` of 0 or less answers 400 `parameter_invalid_integer` "This value must be greater than or equal to 1.", where it answered "Invalid positive integer".

17. **A connected seller's capabilities activate on the clock.** A v2 account created with its onboarding details reads `card_payments` `pending` until `/_rystic/clock` is 85 seconds past its `created`, then `active`, with or without a bank. Once it holds an external account, its `payouts` read `pending` from 18 seconds and `active` from 85, and without one they stay `restricted`. Each capability whose status moves sends its `v2.core.account[configuration.<merchant|recipient>].capability_status_updated` event, naming no request. Deactivating a configuration sets its capabilities to null, and re-applying it leaves them null. A `card_payments` re-requested after that reads `active` at once on an account past its activation, with `stripe_balance.payouts` `active` too when it holds a bank, so the seller charges again; on an account not yet activated it reads `pending`. Advance the clock by 85 seconds or more, or run with `RYSTIC_PACE=1`, to reach activation.

18. **A charge as a seller is refused as Stripe refuses it.** A PaymentIntent created with `confirm=true` under `Stripe-Account` or `Stripe-Context` answers 400 "Your account cannot currently make charges…" while the seller's `card_payments` is pending, or when its merchant and recipient configurations were both deactivated. It answers 400 "You cannot create a charge on a connected account without the `card_payments` capability enabled." when only its merchant configuration was deactivated, re-applied or not. Either way the intent is still created, and the error carries it with status `requires_payment_method`, beside the payment method. An unconfirmed create is not refused. A `POST /v1/charges` under either header answers the same "Your account cannot currently make charges…" 400 while the seller's `card_payments` is not active, with no PaymentIntent and no charge in the error.

19. **A renewal that would run past `cancel_at` bills only up to it.** When the clock renews a subscription whose `cancel_at` falls before the next period end, the item's period ends at `cancel_at` and the renewal bills that stub as a proration, paid an hour later, before the subscription cancels at `cancel_at`. Before, the renewal billed the whole period.

20. **A scheduled cancellation keeps its request time in `canceled_at`.** A subscription with `cancel_at_period_end` or `cancel_at` that the clock ends now reads `canceled_at` as the time of the update that asked for it, `ended_at` as the end, and keeps `cancel_at` and `cancel_at_period_end`, as Stripe documents. A `DELETE` while such a request is pending keeps its `canceled_at` and clears `cancel_at` and `cancel_at_period_end`, as Stripe does. Before, `canceled_at` read the end.

21. **A subscription create takes `trial_end` and `trial_settings`.** `POST /v1/subscriptions` now accepts `trial_end`, starting a trial that ends then and needs no payment method, and `trial_settings[end_behavior][missing_payment_method]` (`cancel`, `create_invoice` or `pause`), which `trial_settings` stores. Both were refused with `parameter_unknown` before. A trial's items run from its start to `trial_end`, and its `billing_cycle_anchor` is `trial_end` unless the create sets one.

22. **Each subscription item renews on its own interval.** A yearly item beside a monthly one is billed once a year and the monthly one every month, each renewal invoice carrying only the items whose period ended. An item added with `POST /v1/subscription_items` or an update's `items[]` takes the subscription's current period: that of an item with the same interval, the trial's during a trial. Before, every item renewed at the earliest item's period end, and an item added after the first renewal started at the subscription's first period, so the next clock move wrote a renewal dated in the past.

23. **A subscription keeps its last item.** An update whose `items[]` would leave a subscription with no item, and `DELETE /v1/subscription_items/{item}` on its only item, answer Stripe's 400 `invalid_request_error` "A subscription must have at least one active plan. To cancel a subscription, please use the cancel API endpoint on /v1/subscriptions." and change nothing. Before, both went through.

24. **A subscription whose payment fails goes `past_due`, and `trial_end` moves the trial.** When a `charge_automatically` subscription's invoice finalizes with no payment method, as at a trial's end under `missing_payment_method=create_invoice` with no card, its payment fails: the invoice stays open with one attempt, the subscription reads `past_due` and the customer `delinquent`, and `invoice.payment_failed` goes out; paying the invoice returns both. A later `trial_end` on a trialing subscription moves `billing_cycle_anchor`, the period end and a period-end `cancel_at` with it and bills the added days on a paid zero invoice, and one at or before the subscription's clock answers Stripe's 400 with `param` `trial_end`. A trial's invoice lines read `Free trial for <quantity> × <product>`. Before, the subscription stayed `active` with the invoice open, and a `trial_end` update only stored the new time, or ended the trial when it was past.

25. **A subscription period ends on its anchor's day of the month.** Periods are counted from `billing_cycle_anchor`, each ending on the anchor's day or the month's last day when the month is shorter, so a subscription anchored on Jan 31 renews on Feb 28, Mar 31, Apr 30 and May 31, and a yearly one anchored on Feb 29 renews on Feb 28. A resume under `billing_cycle_anchor=unchanged` aligns to the same days. Before, a Jan 31 anchor renewed on Mar 3 and drifted from there.

26. **Subscriptions bill prorations for partial periods.** A create whose `billing_cycle_anchor` is ahead, with no trial, runs its first period to the anchor and bills it as a proration of the interval that ends there, with its own invoice item. Unless `proration_behavior` is `none`, an item added to an `active` or `past_due` subscription leaves a pending proration for the rest of the period, sent as `invoiceitem.created`, which the next renewal bills ahead of the new period; `POST /v1/subscription_items` now also sends `customer.subscription.updated`. A renewal into a period shorter than the price's interval, after `billing_cycle_anchor=now` on an update or a cleared `cancel_at`, bills that period as a proration, and deleting an item drops its pending proration. A renewal of a monthly and a yearly item together is dated from the last monthly renewal. Before, the first period ran a full interval from the anchor, and an added item was billed nothing for the part of the period it joined.

27. **A resume of a subscription that is not `paused` answers Stripe's message.** `POST /v1/subscriptions/{id}/resume` on a `trialing`, `active` or other subscription that is not `paused` answers 400 `invalid_request_error` with "You can only resume a subscription if it is `paused`.", as Stripe does. Before, the message named the subscription and its status.

28. **A resumed subscription stays `paused` until its resume invoice is paid.** `POST /v1/subscriptions/{id}/resume` with `billing_cycle_anchor=now` (the default) now answers the subscription still `paused`, with a `pending_update` and an open `subscription_cycle` invoice for the period the resume starts, as Stripe does; the invoice is not charged on its own, and each resume writes another. Once you pay that invoice, or mark it uncollectible, the subscription reads `active` from the resume and sends `customer.subscription.resumed`, `customer.subscription.pending_update_applied` and `customer.subscription.updated`. A resume with `billing_cycle_anchor=unchanged` still reads `active` at once with no invoice. Before, every resume read `active` at once with no invoice.

29. **A subscription's trial ends when the clock reaches its `trial_end`.** Three days before (at once for a shorter trial), `customer.subscription.trial_will_end` goes out, after `invoice.upcoming` when the end will bill. At `trial_end` a subscription with a card, a `send_invoice` one, or one under `trial_settings[end_behavior][missing_payment_method]=create_invoice` reads `active` and renews into a `subscription_cycle` draft that finalizes an hour later: a `send_invoice` one is sent, a card pays a `charge_automatically` one, and with no card its payment fails and the subscription goes `past_due`; a `billing_cycle_anchor` after `trial_end` bills a proration up to it first. With no card, `pause` pauses it and `cancel` cancels it, and a later resume starts its period at the resume (with `billing_cycle_anchor=unchanged`, on the anchor's schedule), billing nothing for the paused time. `trial_end=now` on an update bills the new period at once, and `cancel_at_period_end` then cancels at that period's end. Every event the clock sends, a renewal's and a `cancel_at`'s included, is now stamped at the instant it was due, even when one clock move jumps past it. Before, a subscription read `trialing` past its `trial_end`.

30. **A resume that keeps the anchor bills the rest of the period.** `POST /v1/subscriptions/{id}/resume` with `billing_cycle_anchor=unchanged` still reads `active` at once with no invoice, and, unless `proration_behavior` is `none`, leaves a pending proration for the rest of the period it resumes in, which the next renewal bills with the period after it. The resume now sends `customer.subscription.updated` and `customer.subscription.resumed`. Before, the paused part of the period was free and the resume sent no event.

31. **A subscription update cannot touch another subscription's item.** An update whose `items[]` names an item of another subscription, or an ID no item has, answers Stripe's 400 `invalid_request_error` "No subscription item with this ID (`<id>`) on the subscription." and changes neither subscription. Before, it deleted or changed the other subscription's item.

32. **Subscriptions on a test clock renew when the clock advances.** A customer created with `test_clock` and its subscriptions live on the clock's `frozen_time`: their `created`, periods, trial dates and invoices read it, and each invoice carries the clock's `test_clock`. When an advance completes, every renewal, trial warning and end, draft finalize and `cancel_at` due in the advance happens at its own instant on the clock, while the events keep real time and the PaymentIntent and charge that pay an invoice carry no `test_clock`. `/_rystic/clock` moves only the subscriptions on no test clock, and one clock's advance moves only its own. Deleting a test clock cancels the subscriptions on it and deletes its customers.

33. **A test clock's advance completes.** After `POST /v1/test_helpers/test_clocks/{id}/advance`, the next API request still reads the clock `advancing` with its old `frozen_time`, and the request after it reads the clock `ready` at the requested time with `status_details` `{}`, so it can be advanced again. Only requests the twin hands to a handler count, a world timeline call to an API route included: `/_rystic/` control calls, and clock jumps and paced ticks that fire no timeline call, leave the clock advancing. An advance sent while the clock is still advancing answers 429 "Test clock advancement underway - cannot perform modifications", as Stripe's does, where it answered 400 and the clock never left `advancing`.

34. **A v2 account's writes emit Stripe's account events.** A create emits `v2.core.account.created` plus a part event for each part it sets (`v2.core.account[identity].updated`, `[defaults].updated`, `[configuration.merchant].updated`, `[configuration.recipient].updated`, `[configuration.customer].updated`) and a `capability_status_updated` with `data.updated_capability` for each capability status it sets; an update emits `v2.core.account.updated` and the merchant part and capability events it causes; a close emits `v2.core.account.closed`. `GET /v2/core/events` lists them, never `/v1/events`, and a part event's `related_object.url` carries `?include=<part>`, which `GET /v2/core/accounts/{id}` accepts, so stripe-node's `fetchRelatedObject` resolves it. An event's `changes` takes Stripe's shape: a created event's is `{before: null, after: <the account's top-level fields>}`, a closed event's the reverse, an updated event's the moved top-level fields, and a part or capability event's the whole part on each side. A part can hold fewer members than Stripe's, which fills some objects and lists every payment method capability.

35. **`GET /v2/core/events` answers 10 events a page by default.** A list sent without `limit` answers 10 events, as Stripe's does, and its relative `next_page_url` carries the page token and only the parameters the request sent, so the next page reads 10 again.

36. **Balance settings follow the seller's activation.** Under `Stripe-Account` or `Stripe-Context`, `GET` and `POST /v1/balance_settings` read `payments.payouts.status` `enabled` and `payments.debit_negative_balances` `false` once the account's `stripe_balance.payouts` capability is active, as Stripe's do, and `disabled` and `true` before.

37. **Acting as a closed account is refused.** A `Stripe-Account` or `Stripe-Context` header naming a v2 account the key closed answers 403 `account_invalid` (`api_error`), the answer for an account the key does not hold, on reads and writes alike, and the request creates nothing, as Stripe's does. The twin used to act as the closed account.

38. **A destination charge names its seller, transfer group, transfer and fee.** A destination charge's Charge carries `destination` (the connected account), `transfer_group` (`group_` plus the PaymentIntent's ID when the request names none, also on the PaymentIntent), `transfer` (its `tr_` transfer) and, with an `application_fee_amount`, `application_fee` (its `fee_` ID). The twin names `transfer` and `application_fee` at the charge, where Stripe names them about 6 seconds later.

39. **A destination charge lists the platform's transfer and application fee.** After a destination charge, the platform's `GET /v1/balance_transactions` lists a `transfer` row for minus the transferred amount, its source the charge's `tr_` transfer, and, with an `application_fee_amount`, an `application_fee` row for plus the fee, its source a `fee_` application fee, beside the `charge` row, so the list adds up to the platform's balance as Stripe's does.

40. **A destination charge tells the connected account about its payment.** The `py_` payment a destination charge makes emits `payment.created` to the connected account: the account's `GET /v1/events` lists it, with `data.object` the payment and `request` `{id: null, idempotency_key: null}`, and the platform's Connect endpoints receive it, while the platform's own event list never shows it, as Stripe's does. An endpoint's `enabled_events` accepts `payment.created`. Stripe lists the event about 3 s after the charge, and the twin emits it at once.

41. **A destination charge's refund can reverse the transfer and the application fee.** A refund with `reverse_transfer=true` takes its share of the transfer back from the connected account: the account lists a `payment_refund` row and a `pyr_` refund of its payment, the platform a `transfer_refund` row, and the refund's `transfer_reversal` names a `trr_` reversal. With `refund_application_fee=true` it returns its share of the application fee: the account lists an `adjustment` row and the platform an `application_fee_refund` row. Each refund's shares are its own proportion of what the charge captured, rounded half up and never more than remains, and the refund whose reversal leaves the transfer fully reversed returns the rest of the fee, as Stripe's do. Without `refund_application_fee` the account keeps paying the fee, so its balance can go negative, as Stripe's does. A refund naming neither moves only the platform's balance.

42. **A connected account's events are its own.** An event a request causes while acting as a connected account (`Stripe-Account` or `Stripe-Context`) belongs to that account: its v1 snapshot carries `account` and `context` naming it, its v2 rendering `context`, and `GET /v1/events` and `GET /v2/core/events` list and retrieve it only when read as that account, as Stripe's do. Every other event stays the platform's and names no account. A webhook endpoint created with `connect=true` is a Connect endpoint: it renders `application` (a stand-in `ca_` ID, where Stripe names the platform's own Connect application) and takes connected accounts' events alone, while every other endpoint takes only the platform's. No endpoint renders `connect`.

43. **A payout writes its balance transaction.** `POST /v1/payouts` writes a `payout` balance transaction for minus its amount, the one the payout's `balance_transaction` names, so `GET /v1/balance_transactions/{id}` retrieves it and the list, filtered by `type=payout` or `source` too, includes it. An account's transactions now add up to its balance after a payout.

44. **A payout goes to the account's own bank account and arrives on a day boundary.** A payout made as a connected account names, as its `destination`, the bank account `POST /v1/accounts/{account}/external_accounts` returned for that account, where the twin used to name a new `ba_` ID on each payout. The platform's payouts all name one stand-in bank account. `arrival_date` is midnight UTC of the payout's day, as the measured payouts read, where it used to equal `created`.

45. **A connected account reads the payment a destination charge makes.** Under `Stripe-Account` or `Stripe-Context`, the connected account retrieves its `py_` payment with `GET /v1/charges/{id}` and finds it in `GET /v1/charges`, as Stripe's does. The payment is a Charge for the transferred amount whose `source_transfer` names the platform's transfer and whose `balance_transaction` is the account's `payment` row.

46. **A v2 account's identity never echoes an ID number.** A stored identity keeps only each ID number's type, `us_ssn` followed by `us_ssn_last_4`, never its value, in `include=identity` reads and in the `v2.core.account[identity].updated` event alike, and its address countries read upper-case (`US`), as Stripe's do.

47. **`include=requirements` lists what an account still owes.** A v2 account read, listed, created or updated with `include=requirements` answers `{entries, summary}`. An onboarded seller lists `external_account` until it holds a bank account, and the identity checks Stripe's verification works through until its activation. Once it holds a bank account and is active, it answers `entries: []` with `summary: null`. An account not onboarded whole lists only `external_account`, where Stripe names each missing field. `future_requirements` reads `null`.

48. **A DELETE takes its parameters in the query string.** `DELETE /v1/subscriptions/{id}?invoice_now=true&prorate=true`, sent without a body as Stripe's SDKs send a DELETE, now cancels the subscription; it answered 400 `parameter_unknown`. A DELETE sent without a body reads its documented parameters from the query string, and any other query parameter is still refused. A DELETE that carries a form body still takes its parameters from the body alone.

49. **The twin's clock acts as the platform.** What the clock settles (a subscription's renewal, trial warning, trial end and `cancel_at`, a Checkout Session's expiry, a test clock's completion, a seller's capability moves) writes its PaymentIntent, charge, balance entry and events to the platform, whichever account the request that brings the clock there acts as. They used to land on the connected account that request named in `Stripe-Account` or `Stripe-Context`.

## v0.0.15 — 2026-10-18

1. **A v2-native event stays off `/v1/events`.** An event of a `v2.` type, such as the event destination ping, is logged only as a `v2.core.event`: `GET /v1/events` no longer lists it, its v1 ID no longer retrieves it, and no webhook endpoint receives it, as on Stripe.

2. **A failed webhook delivery is retried, each retry signed again.** A delivery your endpoint or thin event destination does not answer with a 2xx is retried three times, 30 min, 1 h and 2 h after each failure, or as soon as `/_rystic/clock` moves the clock past a retry's due time. Each retry carries the same body and a new `Stripe-Signature`, so `constructEvent` and `parseEventNotification` accept it, and none goes to an endpoint deleted or disabled by then. The delays are assumed, since Stripe documents only three sandbox retries over a few hours. The `delivery` report on `GET /_rystic/egress` adds `retries_armed`, `next_retry_at` and `next_retry_clock`; `pending` still counts a retry waiting out its delay, so a test that waits for `pending` to reach 0 after a failed delivery waits through the retries unless it moves the clock past them.

3. **An event's `pending_webhooks` counts the endpoints yet to take it.** A v1 event's `pending_webhooks` is the number of enabled webhook endpoints subscribed to it when it was logged, less each that has answered a delivery with a 2xx, so `GET /v1/events?delivery_success=false` lists an event still waiting on a retry or whose retries all failed, where it used to read `0` always. Thin event destinations count nothing; with `RYSTIC_WEBHOOK_DELIVERY` off, nothing lowers the count.

4. **A card confirm at create without a `return_url` is refused as Stripe refuses it, a charge's balance transaction can be read, and the charge sends `charge.updated`.** `POST /v1/payment_intents` or `/v1/setup_intents` with `confirm=true` and neither `payment_method_types` nor `automatic_payment_methods` answers Stripe's 400 without a `return_url` and creates no intent. A succeeded charge names its balance transaction at the confirm and, about 3 s later on the twin's clock, sends one `charge.updated` whose `previous_attributes` carry `balance_transaction`; a refund within 2.5 s of the charge supersedes that event, and a refund names its own transaction at create. `GET /v1/balance_transactions/{id}` serves a charge's or a refund's transaction with Stripe's `amount`, `fee`, `net`, `type` and `status`, and `GET /v1/balance_transactions` lists them, filtered by `source` or `type`. Not yet as Stripe: a create with `automatic_payment_methods[enabled]=true` and no `allow_redirects=never` is accepted without a `return_url`, the `/confirm` routes still accept an intent created without `payment_method_types` and no `return_url`, the charge's transaction reads at once where Stripe attaches it about 3 s later, and the event's `previous_attributes` carry no `receipt_url`.

## v0.0.14 — 2026-10-16

1. **A card can be blocked, and Radar's default lists stay fixed.** A card whose fingerprint is on the default `card_fingerprint_blocklist` is refused with `402 card_declined`, as a blocked email is, by PaymentIntent create with `confirm=true`, confirm and charge create, and pays again once the item is deleted. Each test card reads one `card.fingerprint` on every path, so `tok_visa`, `pm_card_visa`, a saved card and a charge's card agree. Renaming or deleting one of the 24 default lists answers Stripe's `400`.

2. **Charge search filters by its query.** `GET /v1/charges/search` now returns only the charges that match the query, as Stripe does: `amount`, `created`, `currency`, `customer`, `disputed`, `refunded`, `status`, `metadata['key']`, `billing_details.address.postal_code` and the card fields under `payment_method_details`, joined by `AND` or `OR`. A `metadata['order_id']:'…'` lookup works, and an unsupported field or operator, an unquoted string or a query that mixes `AND` and `OR` answers 400 with Stripe's message.

3. **Every refund route refuses what Stripe refuses.** `POST /v1/refunds`, `POST /v1/charges/{charge}/refunds` and `POST /v1/charges/{charge}/refund` now answer the same 400s: `charge_disputed` for a disputed charge, `charge_already_refunded` for a fully refunded one, and Stripe's "Refund amount ($20.00) is greater than …" message for an over-refund. After a partial capture, a refund is limited to the captured amount, and a refund with no amount refunds only that.

4. **`GET /_rystic/egress` shows the twin's URLs.** An event's `hosted_invoice_url`, `invoice_pdf` and `receipt_url` in the egress log are on the origin you read it on, as in the delivered event and `GET /v1/events`, never on `invoice.stripe.com` or `pay.stripe.com`; `?raw=1` returns Stripe's hosts.

5. **Paying an invoice charges the customer's default card, and invoice payments are served.** `POST /v1/invoices/{invoice}/pay` with no `payment_method` charges the invoice's `default_payment_method`, else the customer's `invoice_settings.default_payment_method`, and the PaymentIntent and charge it makes name that card, so a default bypassPending card's funds land in `available`. `GET /v1/invoice_payments` (filtered by `invoice`, `status` or `payment`) and `GET /v1/invoice_payments/{invoice_payment}` read the payment that names the invoice's PaymentIntent.

6. **v2 events and event destinations.** `GET /v2/core/events` lists and `GET /v2/core/events/{id}` retrieves every event your requests cause, as `v2.core.event` objects under their own `evt_test_` IDs, filtered by `object_id`, `types` or `created`, newest first and paged by `next_page_url`. `/v2/core/event_destinations` creates, lists, retrieves, updates, deletes, enables, disables and pings destinations; with `RYSTIC_WEBHOOK_DELIVERY=on`, a thin destination receives each event it enables as a signed thin notification. `GET /v1/events` lists the same events as v1 snapshots, now including those from a customer update, a declined payment, invoice items, invoice updates, finalizing, paying, voiding and marking uncollectible, credit notes, payouts and PaymentMethod updates and detaches.

7. **Subscriptions bill their first invoice, payouts read paid, and each event's changes, reason and order match Stripe.** A subscription create writes its first invoice, and with `charge_automatically` and a default card it pays it at once: `latest_invoice` retrieves a paid invoice with its PaymentIntent and charge, and the create sends the nine events Stripe sends, in Stripe's order. `cancel_at_period_end=true` sets `cancel_at` to the period end, and the subscription ends there when the clock reaches it. A created payout reads `paid` on every later read, as test mode does, so canceling it answers "Payouts can only be canceled while they are pending." Events carry `changes` only where Stripe's do, and `payout.created` and a new refund's settling `refund.updated` and `charge.refund.updated` name no request. A declined payment, invoice pay and a credit note's refund emit in Stripe's order, and an invoice item added to an invoice sends `invoice.updated`. Event destinations answer `events_from: ["@self"]`, refuse an `.invalid` URL, an unknown `snapshot_api_version` and an `include` other than `webhook_endpoint.url`, and an unknown v2 event answers "No such event: '…'".

8. **stripe-node pages and filters v2 events.** `next_page_url` and `previous_page_url` on `GET /v2/core/events` and `GET /v2/core/event_destinations` are relative paths, as Stripe answers them, so an SDK's autopaging follows them; the indexed `types[0]=…&types[1]=…` the SDK sends filters as Stripe does; and an event's `request.id` names the answer's `Request-Id` when the request carries an `Idempotency-Key`.

9. **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.

10. **Webhook endpoints and event destinations are one list, and late events arrive late.** A v2 snapshot destination lists in `GET /v1/webhook_endpoints` and a v1 endpoint in `/v2/core/event_destinations`, and a delete on either route removes both. `charge.updated` follows a confirmed charge about 3 s later, unless the charge is refunded within 2.5 s of the confirm, which drops it for good, and `payout.created` lands after the payout's updates, as Stripe sends them. A destination's signing secret can no longer be derived from its ID.

11. **Subscriptions, items, refunds and charges carry every member Stripe returns.** A subscription now has `plan` and `quantity` on every list and retrieve, even when seeded without them, and has `test_clock`, a `flexible` `billing_mode` by default and the other members Stripe answers; an item has `plan` and `current_trial` and no `billed_until`; a refund names its `customer` and `payment_method` and omits an unset `description`; a manual-capture charge carries `capture_before`. A charge no longer carries `amount_updates`, which Stripe does not return. A Checkout Session created without a `return_url` omits the key, an updated invoice answers `payments` only when expanded, and a seeded payment link's list and update answers no longer carry the unexpanded `line_items` sublist.

12. **Refusals read as Stripe's.** An undeclared query or body parameter answers 400 `parameter_unknown` on every route (`invalid_fields` "Unknown field." under `/v2`), and a refusal on a PaymentIntent or SetupIntent route carries the intent it names. A wrong `Content-Type` on `/v2` answers 415. Card declines use Stripe's current wording, a too-long metadata value is quoted as `xxxx...xxxx`, an unknown subscription item answers "Invalid subscription_item id", an unknown credit grant answers 400 "Invalid credit grant ID", and the v2 event destination refusals list their `invalid_fields`.

13. **A subscription without a card is refused or incomplete, and periods renew.** A `charge_automatically` create with no card answers 400 `resource_missing`, or `incomplete` under `payment_behavior=default_incomplete`. A `send_invoice` subscription's first invoice stays a draft for an hour, and each period end drafts a renewal invoice that is paid an hour later. Finalizing an invoice creates its PaymentIntent and invoice payment, which pay reuses and a void cancels.

## v0.0.13 — 2026-10-15

1. **A credit grant writes its credit balance transaction.** Creating a credit grant appends one `billing.credit_balance_transaction` of `type: credit` with `credit.type: credits_granted`, the grant's amount, its ID as `credit_grant` and its `effective_at`, as Stripe does. `GET /v1/billing/credit_balance_transactions?customer=…` lists it and `GET /v1/billing/credit_balance_transactions/{id}` returns it. Voiding or expiring a grant still appends no transaction.

2. **The balance moves with your money, payouts draw on it, and refunds of uncaptured charges release them.** `GET /v1/balance` answers a `usd` entry in `available` and `pending` from the first request. A captured card payment adds its amount less Stripe's 2.9% + 30¢ fee, to `available` for the `tok_bypassPending` / `pm_card_bypassPending` test card and to `pending` for every other card; a PaymentMethod made from `tok_bypassPending` is the card ending 0077, and once saved it still settles to `available` on every PaymentIntent path. A refund takes its amount back out, an invoice paid by card credits the balance as a card payment does, a credit note's `refund_amount` debits it as a refund does, and a payout draws on `available`: a payout above it, or in a currency the balance does not hold, answers 400 `balance_insufficient`, and canceling a pending payout returns its funds. Refunding a charge created with `capture=false` releases the authorization, as Stripe does, and capturing it afterwards answers `charge_already_refunded`. A charge a PaymentIntent holds for capture is refused with Stripe's message to cancel the PaymentIntent instead. A `pm_card_pendingRefund` payment refunds `pending`, and canceling a card refund answers Stripe's "Canceling this refund is unsupported."

3. **Your key works as the Basic username.** Every route reads the secret key from `Authorization: Bearer <key>` or as the HTTP Basic username, so Stripe's curl form `curl -u sk_test_…:` now answers the invoice, invoice item, payout, product and credit balance transaction lists, which refused it with 401.

4. **Charges carry `amount_updates`.** Every charge the twin answers, on create, retrieve, update, capture, refund, list and search, holds `amount_updates: []`, as a Stripe test-mode charge does under `2026-06-24.dahlia`, so code that reads the field no longer finds it missing.

5. **A chargeback can be looked up.** A charge paid with `tok_createDispute`, or a PaymentIntent paid with `pm_card_createDispute`, opens a `needs_response` Dispute as Stripe does: the charge reads `disputed: true` and names it in `dispute`, `charge.dispute.created` is sent, and `GET /v1/disputes`, `GET /v1/disputes/{dispute}` and `GET /v1/charges/{charge}/dispute` return it. `POST /_dispute` stores the Dispute it opens, `design_studio`'s disputed charge carries its Dispute, and a charge with no dispute answers "No dispute for charge" rather than "No such charge".

6. **Hosted invoice, receipt and payment link URLs lead to the twin.** A finalized invoice's `hosted_invoice_url` and `invoice_pdf`, a credit note's `pdf`, a charge's `receipt_url` and a payment link's `url` are on the origin you called, as a Checkout Session's `url` already is, so an agent that hands a shopper the link, or fetches it, never reaches production Stripe. The twin serves no page at those paths yet: they answer 404.

7. **An invoice item is a line of its invoice, paying an invoice charges its card, and a credit note moves money.** An invoice item created with `invoice` set to a draft becomes one of its lines, so the draft's `subtotal`, `total` and `amount_due` include it and finalize answers `open` with that amount due; updating or deleting the item moves or drops its line, and `pending_invoice_items_behavior=include` pulls the customer's pending items onto a new draft. An item priced through `price_data` costs `unit_amount` times `quantity`. Paying an open invoice charges `payment_method`, else the invoice's `default_payment_method`, through a succeeded PaymentIntent and charge, and `expand[]=payments` on the invoice lists the `invoice_payment` naming that PaymentIntent. Invoice search takes single-quoted values such as `customer:'cus_…' AND status:'paid'`. On a paid invoice a credit note's `refund_amount` refunds the invoice's charge and is listed under its `refunds`, `credit_amount` lowers the customer's `balance` and names a `customer_balance_transaction`, and an amount sent to none of them answers Stripe's 400; a credit note whose invoice is not open can no longer be voided.

8. **`pm_card_visa` becomes a saved card.** Attaching `pm_card_visa` or another test payment method to a customer saves a new `pm_…` card of that brand to them, and a declining one (`pm_card_chargeDeclined`, …) answers `402 card_error` as Stripe does. A PaymentIntent that names a test payment method now names a new `pm_…` instead of the token, and a payment confirmed with a customer and `setup_future_usage` saves that card, so the customer's payment-method list returns it. Confirm takes `setup_future_usage`.

## v0.0.12 — 2026-10-14

1. **The page describes the hosted portal and disputes as the twin serves them.** The v0.0.10 notes no longer say the twin serves no portal page (`/p/session?secret=…` opens a working portal), and the Limitations section names `POST /_dispute`, the world action that opens a dispute on a charge.

2. **The state UI is included on Free.** `/_rystic/ui` is available on every plan, including Free and the no-signup trial. The Pro-only verified UI download is a separate feature.

3. **A JSON body is refused, as Stripe refuses it.** A `POST` whose nonempty body is sent under any `Content-Type` other than `application/x-www-form-urlencoded` now gets Stripe's 400 `invalid_request_error` ("Invalid request (check that your POST content type is application/x-www-form-urlencoded)…") instead of being read as form. Send form-encoded bodies, as the Stripe SDKs do; a bodiless `POST` such as `/cancel` is unaffected.

4. **Setting the clock never stamps records before the timeline events it fired.** When `POST /_rystic/clock {"time": ...}` fires timeline events due at its target, the next request's records stamp after theirs instead of reusing their instant; a set past every event still stamps one millisecond past its target, as before.

5. **A paced clock keeps wall time, and `rystic run --speed 0` stops it.** Under `RYSTIC_PACE`, each tick moves the clock by the wall time that passed since the last, so a tick missed while the twin was busy no longer makes timeline events fire late. `rystic run` hands the twin only the pace it resolved, so `--speed 0`, a world without events, `--tape` and `--live` keep the clock manual even when your shell exports `RYSTIC_PACE`.

6. **Billing meters, alerts, credit grants, test clocks and 64 operations in all.** The twin now serves billing meters (with event summaries), meter event adjustments, billing alerts, credit grants and the credit balance, customer portal configurations, test clocks, product features and subscription items. It also serves products list, update and delete; price update and search; payment methods list; charge and PaymentIntent search; charge capture and refunds on a charge; refund cancel; subscription resume, migrate and discount delete; and the PaymentIntent `increment_authorization`, `apply_customer_balance` and `verify_microdeposits` and SetupIntent `verify_microdeposits` actions. Error bodies from API routes carry `request_log_url`, and `doc_url` wherever they carry a `code`, as Stripe's do; a 401, a refused non-form body and an unknown URL answer without them.

7. **Billing alerts answer `filters: []`, and seeded recurring prices read back whole.** A billing alert created without `usage_threshold[filters]` now answers `usage_threshold.filters` as `[]`, as Stripe does, on create, retrieve, list, activate, deactivate and archive. The setup's recurring prices now carry `type: recurring` and `recurring.interval_count`, `usage_type` and `meter`, so a subscription's embedded price matches Stripe's.

8. **Lists refuse cursors and clamp `limit` as Stripe does.** The billing alerts, credit grants, meters, products and test clocks lists refuse `starting_after` sent with `ending_before`. Credit grants, meters, products and test clocks also refuse a `starting_after` that names no record, and products refuses such an `ending_before`. The alerts, meters, portal configurations and products lists serve `limit=0` and `limit=101`, with the page size clamped to 1..100, and refuse only a non-integer `limit`.

9. **Missing references answer 400, unknown query parameters are refused, and list order is stable.** A body or query parameter naming a record that does not exist answers 400 `invalid_request_error` with code `resource_missing`, as Stripe does, on price create (`product`), credit grant create and portal session create (`customer`, including a deleted one), the refunds list (`charge`, `payment_intent`) and the setup attempts list (`setup_intent`); a billing alert naming no meter answers Stripe's 400 "The provided meter ID does not exist." Meter event summaries and payment method attach check the `customer` before the path record. Twenty-five list and search GETs refuse a query parameter they do not take with 400 `parameter_unknown`, and the customers list takes `expand` and `test_clock`. The seeded charge, PaymentIntent, prices and product are dated before the twin's clock starts, so a record you create heads every newest-first list.

10. **Invoices, credit notes, payouts, disputes, Radar value lists and 46 operations in all.** The twin now serves invoices (create, update, delete, add lines, finalize, pay, void, mark uncollectible, list, search and their lines), invoice items, credit notes (with preview and lines), the account balance, payouts (create, update, cancel, list), dispute reads (`GET /v1/disputes`, a dispute, and a charge's dispute), customer and product search, a customer's payment methods, and Radar value lists and their items. An address on the default `email_blocklist` list refuses a charge, a PaymentIntent confirm or a PaymentIntent create with `confirm=true` that sends it as `receipt_email`, with Stripe's 402 `card_declined` and a failed charge whose outcome names the block list. A Checkout Session's retrieve answers `custom_fields`, `custom_text` and `shipping_options`, as its list does.

## v0.0.11 — 2026-10-12

1. **A clock jump stamps each timeline event at its exact instant.** When a clock move passes timeline events, the records each one makes carry exactly its `at`, to the millisecond, whatever traffic came before it, so one jump leaves the same records as stepping the clock to each event in turn.

2. **Hosted Checkout completes subscription sessions.** Subscribing on a `subscription`-mode session's page creates the customer and a subscription with the session's prices, quantities and `subscription_data[metadata]`, completes the session and redirects to `success_url`, sending `customer.created`, `customer.subscription.created` and `checkout.session.completed` to your registered endpoints in that order. Payment and setup sessions still answer 501.

3. **A subscription ends at its `cancel_at`.** Once the twin's clock reaches a subscription's `cancel_at` (set through the API or the customer portal), the subscription reads `canceled` with `ended_at` set and `customer.subscription.deleted` goes to your registered endpoints, so a `POST /_rystic/clock` advance past it plays out the cancellation.

## v0.0.10 — 2026-10-12

1. **The scripts in this archive are MIT-licensed.** `check.sh` and 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.txt` no longer covers them; it still governs the twin itself.

2. **Webhook endpoints can receive their events.** Boot with `RYSTIC_WEBHOOK_DELIVERY=on` and every event your requests trigger is POSTed to each enabled endpoint registered through `POST /v1/webhook_endpoints` whose `enabled_events` names it or `*`, signed with that endpoint's `secret` in `Stripe-Signature` at wall-clock time so `stripe.webhooks.constructEvent` accepts it however the twin's clock is pinned; without the variable nothing is POSTed.

3. **A world can carry its own timeline.** A `--seed` file, or a complete world document posted to `POST /_rystic/seed`, may add `credentials`, binding presented credentials to actors, and `events`: 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-level `credentials` or `events` key the twin used to ignore now takes effect. `RYSTIC_PACE=<speed>` plays the clock, and the timeline, in real time, and an event's `at` may be `+<duration>`. `/_rystic/inject`, `--inject` and `RYSTIC_INJECT` now need `state_set` instead of `scenarios`.

4. **A timeline call can hand what it minted to later events.** A `call` event may add `bind`, `{"<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.

5. **A Checkout Session's url opens the twin's hosted Checkout page.** `url` now leads back to the twin instead of `checkout.stripe.com`, and the page shows the session's line items, total and email with a link back to `cancel_url`. Submitting it is not simulated yet: it answers 501 and the session stays open.

6. **`GET /v1/events` lists the events your requests trigger.** Creating a customer, a charge or any other object that triggers an event now also writes that event to `/v1/events`, under the same ID its webhook delivery carries, so `GET /v1/events` and `GET /v1/events/{id}` return it beside any seeded events.

7. **A world can open a dispute.** `POST /_dispute` opens a dispute on a captured charge, the way a cardholder's bank does: the charge reads `disputed`, a refund is refused with `charge_disputed`, and `charge.dispute.created` reaches the egress log and every endpoint registered for it. A world's timeline schedules it like any other call.

8. **Products, prices, subscriptions, customer portal sessions and meter events.** The twin serves 14 more Stripe routes: create and retrieve a product; create, retrieve and list prices; create, retrieve, update, cancel, list and search subscriptions; update a subscription item; create a customer portal session and a billing meter event. A subscription's create, update and cancel emit `customer.subscription.created`, `.updated` and `.deleted`. A portal session's url leads back to the twin's customer portal (item 12).

9. **A clock jump fires each timeline event at its own time.** When one `POST /_rystic/clock` or paced tick passes several events, each fires with the clock at its `at`, 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.

10. **Webhook signatures can follow a shifted receiver clock.** `POST /_rystic/clock {"wall_offset_seconds": N}` signs every webhook delivery's `Stripe-Signature` `t=` N seconds past the wall clock, so a receiver whose own clock you moved forward (as `sandbox clock -advance` does) still accepts it; `0` or `/_rystic/reset` puts it back.

11. **A timeline call can carry raw bytes, XML or a JSON array.** A `call` event may send `body_text` or `body_base64` instead of `body`; 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 in `body_text`. A `body` may 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`.

12. **A customer portal session's url opens the twin's customer portal.** `url` leads back to the twin at `/p/session?secret=…`, which lists the customer's subscriptions with their seats and renewal date. Changing seats, canceling at the end of the billing period (`cancel_at` set, `cancel_at_period_end` left `false`, as Stripe's portal does) and taking a cancellation back each update the subscription and send `customer.subscription.updated` to your registered webhook endpoints.

## v0.0.9 — 2026-10-11

1. **`check.sh` fails fast on a port that never answers.** Its port check and boot wait give each `/_rystic/version` probe two seconds, so an address where something accepts connections but never replies fails the check with a message instead of hanging.

2. **Answers carry the fields Stripe's API reference fixes.** Where a response would leave out, or send null for, a field the reference fixes for that route and status (a required constant such as `object`, a required field's declared default, a variant's tag), the twin now fills it in.

3. **Line items are built from the request's `line_items`.** `GET /v1/checkout/sessions/{session}/line_items` and `GET /v1/payment_links/{payment_link}/line_items` list one line item per `line_items` element of the create, in request order, each with its own `li_` ID, the element's quantity, the product name as its description, a Price (minted from `price_data`, inactive and one-time), and `amount_subtotal` equal to the unit amount times the quantity. On either list, a line naming an existing Price carries that Price with every field a Stripe Price requires. The twin holds no Prices, so the Price's product and creation time come from its ID alone, the same on every session and link that names it, and the rest are defaults: active, one-time, per-unit, in the session's or link's currency (`usd` when the create gives none). Its unit amount stays unknown, so the line's amounts are 0. A session no longer collapses its lines into one, or answers a null currency when every line names a Price. A payment link no longer answers the ID `li_unknown` or amounts of 0 on a line built from `price_data`, and neither list answers a Price that is only an ID.

4. **`check.sh` gives each run its own twin log.** The twin's log is no longer the shared `/tmp/stripe-twin.log`: each run writes it to a path of its own, so two checks running at once on one host no longer overwrite each other's log or report a failure from the other's. When the check leaves the twin running, its last line prints the log's path; when it stops the twin, it removes the log.

## v0.0.8 — 2026-09-30

1. **Charge lists include the full charge details.** `GET /v1/charges` now includes the 24 fields previously available only when retrieving an individual charge, including billing details, payment-method details, outcome, and nullable references. Stored details are preserved, so callers can inspect charges directly from list results.

2. **Retries reuse the original response.** Repeating a POST with the same `Idempotency-Key` and parameters returns the original response without creating another object. Reusing the key with different parameters returns `400 idempotency_error`.

3. **Simulator limitations are easier to find.** The HOWTO now explains unsupported behavior, incomplete object shapes, missing error fields, and scenario-clock differences, and the release notes point to it for current limitations.

4. **Stripe-compatible errors and response headers.** Unknown URLs and trailing slashes now return Stripe's invalid-request envelope. Supported routes include `Request-Id` and `Stripe-Version`, and echo a pinned `Stripe-Version` request header.

5. **Full list objects and successful-card seeds.** PaymentIntent, refund and customer list items retain their full object fields. Seeded PaymentIntent reads include nullable fields present on created records, and customer list projections preserve explicit nulls. Successful card charges in the scenario pack include synthetic payment-method, outcome and balance-transaction details. PaymentIntent confirmation records card details and an authorized outcome on its charge. Key-set checks against the live API check list items, member reads, and seeded records.

6. **Expand related objects on reads.** Charge and PaymentIntent retrieve/list routes and PaymentMethod retrieve accept `expand[]` for their modeled links. Nested paths, multiple paths and list expansions return canonical related objects. Invalid paths return 400 before the handler runs. Null references stay null. Expansion on other routes is not supported.

7. **Immediate test-card declines.**

   Charge creation and PaymentIntent creation/confirmation recognize the measured
   immediate-decline test inputs and return HTTP 402 card errors with the matching
   code, decline code, message, parameter, and input-specific advice/network codes.
   The error includes synthetic request/charge identifiers and the measured
   PaymentMethod shape where applicable. A failed intent retains its
   requires_payment_method status and last_payment_error for subsequent reads.

   The probe generator supports service-declared negative body values and request
   headers. It measures refusal responses separately from optional state readbacks
   and keeps cleanup for prerequisites. A valid confirmation retry verifies that
   the persisted error clears before successful state is stored. Conditional decline-after-attachment
   inputs, failed-charge projection, and retry-limit cancellation remain outside
   this closure.

8. **Parameter validation.**

   Invalid amount syntax, malformed customer email and webhook settings, oversized
   metadata, missing token/customer references, and conflicting payment settings
   return measured Stripe errors before successful writes. Charge currency is
   normalized to lowercase. Missing-source errors precede missing-amount errors,
   and missing parameters and error links use the measured response fields.

   Schema-derived probes independently exercise type, format, bounds, enum,
   map-key/value, unknown-parameter and missing-reference cases. Bounds that the
   live API accepts stay accepted: list limits outside 1–100 are clamped,
   while non-integer limits and created-time bounds are rejected. Complete error
   key sets distinguish absent fields from null fields. These checks do not
   establish validation of every Stripe parameter or every currency's minimum.
   Repeated scalar query values use the last supplied value consistently for
   validation, pagination and created-time filtering. A scoped live receipt and
   seeded timestamp regression tests cover both orderings of duplicate inputs.

9. **The state UI shows the account as the Stripe Dashboard.** On Pro and Enterprise, `/_rystic/ui` opens a live Dashboard view of the twin — Home charts, payments, refunds, customers, payment links, Checkout Sessions, webhooks and events — where you can refund, capture, cancel, edit customers, manage payment links and webhook endpoints and expire sessions through the twin's own API, beside Your integration, Webhook egress and Clock cards.

## v0.0.7 — 2026-09-26

1. **The guide starts from an API key and says what each plan reaches.** `HOWTO.md` now opens with `rystic login rys_…`, says what Free, Pro and Enterprise boot and which control-plane routes each answers, and lists PaymentIntent capture among the 50 routes. `check.sh` finds the API key `rystic login` stored as well as a license file, looked up in the CLI's order, and `rystic features` marks the features and commands that need Pro or Enterprise.

## v0.0.6 — 2026-09-25

1. **A new world, `design_studio`, now the default.** `--scenario design_studio` boots Website Design Contractors' own account: 30 clients as customers, 269 charges since November 2024 each naming its invoice, a declined card paid on a replacement, a refund, a dispute, a bank debit still pending, and seven unpaid invoices as open intents, for the same clients, invoice numbers and dates the studio's Slack, Resend, PayPal and Lob worlds hold. A lease that grants no scenarios boots it instead of `customer_with_card`.

2. **Only `design_studio` ships as a named scenario.** The older scenarios are now test-only fixtures and are no longer accepted by `--scenario`, `RYSTIC_SCENARIO`, or `POST /_rystic/scenario`. Tests and evaluations can load their unchanged register maps and clocks from `sim/testdata/scenarios/` through the control plane.

3. **Self-serve on the metered edition replaces the free edition.** A Rystic account's token runs the metered build: `rystic login rys_…` once, then `rystic run` or `docker 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.

## v0.0.5 — 2026-09-23

1. **A free edition.** stripe-twin now has a free download: no license, one
   fixed world (`customer_with_card`) with reads and writes, and no control plane.
   Get it from https://www.rystic.ai/dl/free/stripe-twin; the licensing docs'
   free edition section says exactly what is in and out.

## v0.0.4 — 2026-09-21

1. **Protected runtime contract 9.** A trusted environment harness can require
   a private gateway credential on every request, including administrative
   routes. This lets the released Stripe HTTP twin serve as the protected
   auxiliary backend in MCP-backed Slack environments without exposing direct
   twin controls to the agent.

The sealed Stripe model and scenario pack are unchanged from v0.0.3. This
release updates the shared runtime; it makes no new Stripe MCP compatibility
or API-fidelity claim. Released multi-episode qualification is recorded
separately from package availability.

## v0.0.3 — 2026-09-14

1. **A fourteenth named world, `busy_account_xxl`.** `busy_account_xl` proves a
   client follows a cursor. This one asks the next question: whether an answer
   still comes out right when it has to be assembled from many pages and many
   objects at once. Same account shape at 2011 live customers — 2305 payment
   intents, 2274 charges, 361 refunds, 535 checkout sessions, 158 payment
   links, 123 setup intents, 505 saved cards, 14 endpoints — with four things
   the smaller worlds do not hold: 81 charged-back charges whose owners carry
   the refunded money on their *other* orders, so the account's net refunded
   figure appears on no single page; six customers holding a run of declines,
   with a unique worst; 240 live subscribers across four plans, sold as
   completed `mode=subscription` Checkout Sessions, beside one-time sessions
   and expired subscription checkouts at the same prices as decoys; and billing
   addresses, so "every customer in country X" is a read of all 2011 customers,
   which no Stripe list route filters on. It boots in about 0.3 seconds and
   holds about 75 MiB. `./stripe-twin --scenario busy_account_xxl`, or
   `curl -X POST localhost:8080/_rystic/scenario -d '{"name":"busy_account_xxl"}'`.

2. **`busy_account_xl` is now big enough to need paging.** Stripe serves at
   most 100 records per list request, and the world shipped in v0.0.2 held
   fewer than that in every collection but customers — so a client could read
   the whole account with one `?limit=100` call per collection and never touch
   `has_more` or `starting_after`. It is now 1011 live customers, 1083 payment
   intents, 1047 charges, 168 refunds, 158 checkout sessions, 134 payment
   links and 131 setup intents, so every one of those answers
   `has_more: true` at `limit=100` and the rest of the account is only
   reachable by cursor. (Webhook endpoints stay at 14, because Stripe caps an
   account at 16.) Everything the smaller world was authored for survives at
   the new size, with the same IDs: `busy_account`'s whole cast, saved cards
   whose last payment sits at 45, 20 or 7 days so an overdue report moves with
   its window, unpaid intents either side of 1-hour, 6-hour and 48-hour
   cutoffs, authorized holds that are cancelable but not unpaid,
   duplicate-shaped charge groups straddling 7-, 14- and 30-day reporting
   windows, open carts either side of 10, 20 and 60 minutes, and live payment
   links either side of 100, 400, 2000 and 5000 cents. Point a client at it
   when you want to find out whether your pagination, cursors and date windows
   are right: `./stripe-twin --scenario busy_account_xl`.

3. **A charged-back charge can no longer be refunded.** `POST /v1/refunds`
   against a charge whose `disputed` is true minted a refund and returned 200,
   where Stripe answers 400 `charge_disputed` — "Charge `<id>` has been charged
   back; cannot issue a refund." The twin now refuses it the same way, for a
   full refund, a partial one and a `payment_intent`-targeted one alike, and in
   the same order live does: an unknown parameter or a non-positive `amount` is
   still rejected first, and the refusal precedes the amount-exceeds-remaining
   check. `busy_account`'s `ch_busy_disputed` is the charge to try it on.

## v0.0.2 — 2026-09-12

1. **Separate authorization and capture works end to end.** `POST
   /v1/payment_intents/{intent}/capture` is routed, and confirming an intent
   created with `capture_method=manual` now holds the authorization at
   `requires_capture` instead of settling it. Partial captures release the
   remainder, over-captures are refused with `amount_too_large`, and the charge
   goes from uncaptured to captured in place — the flow a `capture-later`
   integration actually tests.

2. **`confirm=true` on create does what the docs show.** Creating a
   PaymentIntent with a payment method and `confirm=true` — the one-call form
   the Stripe docs use for off-session charges — now succeeds and mints the
   charge, instead of answering `requires_payment_method` as though the card
   had been declined. Creating with a payment method and no `confirm` correctly
   rests at `requires_confirmation`.

3. **A saved card belongs to its customer.** Confirming a PaymentIntent with a
   PaymentMethod attached to a different customer is refused with Stripe's own
   400, rather than quietly charging one customer's card against another's
   intent.

4. **`disabled=true` disables a webhook endpoint.** `POST
   /v1/webhook_endpoints/{id}` with `disabled=true` flips `status` to
   `disabled`; it was accepted and ignored.

5. **A thirteenth named world, `busy_account_xl`.** `busy_account` is a month
   of traffic over eleven customers, and every question you can ask of it is
   answered by the first page of a list. `busy_account_xl` is the same account
   at 201 customers: the whole `busy_account` cast is still there, with the
   same IDs, plus 190 more customers with mixed histories — 43 saved cards,
   payments spread over the last 45 days, repeat charges that straddle a 7-,
   14- or 30-day reporting window, thirteen open checkout sessions and fourteen
   webhook endpoints over overlapping event sets. Every collection answers
   `has_more: true` on its default page, so it is the world to point a client
   at when you want to find out whether your pagination, your cursors and your
   date windows are right. Boot it like any other:
   `./stripe-twin --scenario busy_account_xl`, or
   `curl -X POST localhost:8080/_rystic/scenario -d '{"name":"busy_account_xl"}'`.

## v0.0.1 — 2026-09-11

The first release of the Stripe twin.

1. **A local Stripe for your integration tests.** 49 routes across 11
   resources: customers, payment intents, setup intents, payment methods,
   charges, refunds, checkout sessions, payment links, webhook endpoints and
   events. Point your Stripe client's `api_base` at the twin and your existing
   suite runs against it unchanged — the Stripe SDKs need no patching. Nothing
   you send it is billed, rate-limited, visible in a dashboard, or shared with
   whoever else on your team is using the same test-mode account.

2. **It never reaches the network, and it needs no key.** Any
   `Authorization` header is accepted, and the twin dials nothing — which is
   the point: a suite that hits `api.stripe.com` fails when Stripe is slow,
   and a suite on a shared test-mode account fails when a colleague clears it.

3. **Seed the world your test needs.** `POST /_rystic/seed` takes a register
   map and lands it directly, so a test starts from the records it needs
   instead of creating them through the API one call at a time.
   `GET /_rystic/registers` lists what this twin accepts a seed for; a
   register the model does not carry is refused with a status rather than
   silently dropped.

4. **The same seed twice is the same world twice.** `POST /_rystic/reset`
   empties the twin and `POST /_rystic/seed-rng` pins the generator, so IDs
   repeat run to run; `POST /_rystic/clock` fixes the twin's notion of now, so
   `created` on every record is a value your test chose. Two identical seed
   flows leave byte-identical `/_rystic/inspect` dumps — which is what makes a
   failing test reproducible rather than merely re-runnable.

5. **Read back the webhooks you would have sent.** Event and webhook egress is
   rendered into an ordered in-memory log (`GET /_rystic/egress`) rather than
   delivered, so you can assert on what would have been sent without standing
   up a receiver or running `stripe listen`.

6. **See what the twin holds.** `/_rystic/ui` serves a live view of the
   registers and the requests hitting them, so a test that fails on a missing
   record shows you the world it actually ran against.

**What is measured, and what is not.** Fidelity is scored differentially
against real `api.stripe.com` in test mode, per response field, not by
eyeball: the last full-suite round was **131 of 161 probes on 2026-09-04**,
and the suite has since grown to 175 probes. Shortfalls recorded for this
release included missing `doc_url` and `request_log_url` error fields,
customer deletion acknowledging without performing, and objects serializing
differently across endpoints. Customer deletion has since been fixed; the
other two remain. Current known limitations are listed in `HOWTO.md` under
Limitations. Rounds are re-run daily and each release states its own number.

**Not in this release.** Subscriptions, products and prices are modeled in
separate twins and are not part of this one; Connect, Tax and Billing are not
modeled at all.

---

Source: https://www.rystic.ai/docs/simulators/stripe/changelog · Markdown: https://www.rystic.ai/docs/simulators/stripe/changelog.md
