Skip to Content

Simulators

Stripe simulator

stripe-twin is a local, stateful simulator of the Stripe API — real lifecycle, conflict and error paths, not stubs.

Quickstart

You need an API key from Account → API keys  (how).

curl -fsSL https://www.rystic.ai/install.sh | sh
rystic login rys_…                                            # your API key
rystic run stripe-twin -d --port 0 --scenario design_studio   # prints the URL it serves on
curl -s http://localhost:<port>/_rystic/version

Expect "product":"stripe-twin" and "version":"0.0.18". An older version means an older install; rystic pull stripe-twin upgrades it. --scenario design_studio boots the same world on every plan. The examples below use localhost:8080; use the port rystic run printed.

On Enterprise, rystic login ./rystic-license.json takes the place of the key (license files).

Point the SDK at it — base URL only, code unchanged:

stripe.api_key = "sk_test_twin"
stripe.api_base = "http://localhost:8080"
const stripe = require("stripe")("sk_test_twin", {
  host: "localhost", port: 8080, protocol: "http",
});

Plans

Every plan (plans) serves all 194 Stripe API routes and every control route but /_rystic/network and /_rystic/ws-drop, which are Pro and Enterprise (routes by plan). Without --scenario the twin boots an empty account, holding only what every Stripe account starts with: a zero usd balance and Stripe’s 24 default Radar value lists. POST /_rystic/reset wipes it back to that; load design_studio with --scenario design_studio or POST /_rystic/scenario.

Auth

The twin reads your key as Stripe does, from Authorization: Bearer <key> or as the HTTP Basic username, and checks no key’s value: send a placeholder sk_test_… so your SDK builds well-formed requests; -u sk_test_twin: in the examples is Stripe’s curl form. Send one on every request: the invoice, invoice item, payout, product and credit balance transaction lists refuse a request with none, as Stripe refuses it on every route.

Coverage

194 routes across 46 resources; the table lists the payments core and the money it moves. GET /_rystic/routes returns every route from a running twin.

ResourceRoutes
Customers (5)POST /v1/customers, GET /v1/customers, GET /v1/customers/{customer}, POST /v1/customers/{customer}, DELETE /v1/customers/{customer}
PaymentIntents (7)POST /v1/payment_intents (incl. confirm=true), GET /v1/payment_intents, GET /v1/payment_intents/{intent}, POST /v1/payment_intents/{intent}, POST /v1/payment_intents/{intent}/confirm, POST /v1/payment_intents/{intent}/capture, POST /v1/payment_intents/{intent}/cancel
Charges (4)POST /v1/charges, GET /v1/charges, GET /v1/charges/{charge}, POST /v1/charges/{charge}
Refunds (4)POST /v1/refunds, GET /v1/refunds, GET /v1/refunds/{refund}, POST /v1/refunds/{refund}
PaymentMethods (5)POST /v1/payment_methods, GET /v1/payment_methods/{payment_method}, POST /v1/payment_methods/{payment_method}, POST /v1/payment_methods/{payment_method}/attach, POST /v1/payment_methods/{payment_method}/detach
SetupIntents (6)POST /v1/setup_intents, GET /v1/setup_intents, GET /v1/setup_intents/{intent}, POST /v1/setup_intents/{intent}, POST /v1/setup_intents/{intent}/confirm, POST /v1/setup_intents/{intent}/cancel
SetupAttempts (1)GET /v1/setup_attempts
Checkout Sessions (6)POST /v1/checkout/sessions, GET /v1/checkout/sessions, GET /v1/checkout/sessions/{session}, POST /v1/checkout/sessions/{session}, POST /v1/checkout/sessions/{session}/expire, GET /v1/checkout/sessions/{session}/line_items
Payment Links (5)POST /v1/payment_links, GET /v1/payment_links, GET /v1/payment_links/{payment_link}, POST /v1/payment_links/{payment_link}, GET /v1/payment_links/{payment_link}/line_items
Checkout Session line itemsGET /v1/checkout/sessions/{session}/line_items; POST /v1/checkout/sessions writes one per line_items element
Payment Link line itemsGET /v1/payment_links/{payment_link}/line_items; POST /v1/payment_links writes one per line_items element
WebhookEndpoints (5)POST /v1/webhook_endpoints, GET /v1/webhook_endpoints, GET /v1/webhook_endpoints/{webhook_endpoint}, POST /v1/webhook_endpoints/{webhook_endpoint}, DELETE /v1/webhook_endpoints/{webhook_endpoint}; connect=true makes a Connect endpoint, which renders application (a stand-in ca_ id) and takes only connected accounts’ events, every other endpoint only the platform’s
Events (2)GET /v1/events, GET /v1/events/{id} — seeded events plus every event your requests trigger, each read only as the account it belongs to
v2 Events (2)GET /v2/core/events (by object_id, types or created, newest first, 10 a page unless limit says otherwise, paged by next_page_url), GET /v2/core/events/{id} — every event your requests trigger, as a v2.core.event under its own evt_test_ id, each read only as the account it belongs to
v2 Event destinations (8)POST /v2/core/event_destinations, GET /v2/core/event_destinations, GET /v2/core/event_destinations/{id}, POST /v2/core/event_destinations/{id}, DELETE /v2/core/event_destinations/{id}, POST /v2/core/event_destinations/{id}/enable, POST /v2/core/event_destinations/{id}/disable, POST /v2/core/event_destinations/{id}/ping; a URL whose host is localhost answers 400 “Invalid URL”, as Stripe’s does, so register 127.0.0.1
Products (2)POST /v1/products, GET /v1/products/{id}
Prices (3)POST /v1/prices, GET /v1/prices, GET /v1/prices/{price}
Subscriptions (6)POST /v1/subscriptions, GET /v1/subscriptions, GET /v1/subscriptions/search, GET /v1/subscriptions/{subscription_exposed_id}, POST /v1/subscriptions/{subscription_exposed_id}, DELETE /v1/subscriptions/{subscription_exposed_id}
Subscription items (1)POST /v1/subscription_items/{item}; POST /v1/subscriptions writes one per items element
Customer portal sessions (1)POST /v1/billing_portal/sessions; the session’s url opens the hosted portal (see Limitations)
Billing meter events (1)POST /v1/billing/meter_events
Invoices (12)POST /v1/invoices, GET /v1/invoices, GET /v1/invoices/search, GET /v1/invoices/{invoice}, GET /v1/invoices/{invoice}/lines, POST /v1/invoices/{invoice}, DELETE /v1/invoices/{invoice}, POST /v1/invoices/{invoice}/add_lines, POST /v1/invoices/{invoice}/finalize, POST /v1/invoices/{invoice}/pay, POST /v1/invoices/{invoice}/void, POST /v1/invoices/{invoice}/mark_uncollectible
Invoice payments (2)GET /v1/invoice_payments, GET /v1/invoice_payments/{invoice_payment}; each names the PaymentIntent that pays its invoice, and a paid_out_of_band pay adds a paid one of type payment_record and cancels the PaymentIntent’s, neither then the default
Invoice items (5)POST /v1/invoiceitems, GET /v1/invoiceitems, GET /v1/invoiceitems/{invoiceitem}, POST /v1/invoiceitems/{invoiceitem}, DELETE /v1/invoiceitems/{invoiceitem}; an item becomes a line of its invoice
Credit notes (8)POST /v1/credit_notes, GET /v1/credit_notes, GET /v1/credit_notes/{id}, POST /v1/credit_notes/{id}, POST /v1/credit_notes/{id}/void, GET /v1/credit_notes/preview, GET /v1/credit_notes/preview/lines, GET /v1/credit_notes/{credit_note}/lines
Disputes (3)GET /v1/disputes, GET /v1/disputes/{dispute}, GET /v1/charges/{charge}/dispute (Limitations 4)
Balance (1)GET /v1/balance (Limitations 3)
Payouts (5)POST /v1/payouts, GET /v1/payouts, GET /v1/payouts/{payout}, POST /v1/payouts/{payout}, POST /v1/payouts/{payout}/cancel; each writes its payout balance transaction (Limitations 3)
Radar value lists (5)POST /v1/radar/value_lists, GET /v1/radar/value_lists, GET /v1/radar/value_lists/{value_list}, POST /v1/radar/value_lists/{value_list}, DELETE /v1/radar/value_lists/{value_list}; the 24 default lists can’t be updated or deleted
Radar value list items (4)POST /v1/radar/value_list_items, GET /v1/radar/value_list_items, GET /v1/radar/value_list_items/{item}, DELETE /v1/radar/value_list_items/{item}; an address on email_blocklist, or a card fingerprint on card_fingerprint_blocklist, refuses a payment that carries it
v2 Accounts (5)POST /v2/core/accounts, GET /v2/core/accounts, GET /v2/core/accounts/{id}, POST /v2/core/accounts/{id}, POST /v2/core/accounts/{id}/close; a request whose Stripe-Account or Stripe-Context names an account the key created acts as that account, whose PaymentIntents, charges, refunds, balance, balance transactions, balance settings, payouts and events are its own, and one naming an account the key closed answers 403 account_invalid, as one the key does not hold does; include=requirements answers {entries, summary}, and a stored identity keeps only each ID number’s type, never its value
External accounts (1)POST /v1/accounts/{account}/external_accounts, a bank account for a v2 account; create only, as GET /v1/accounts/{account}/external_accounts is not served (“Unrecognized request URL”)
Balance settings (2)GET /v1/balance_settings, POST /v1/balance_settings, one per account; a connected account’s read payments.payouts.status enabled and payments.debit_negative_balances false once its stripe_balance.payouts capability is active, disabled and true before

Products

Invoices, invoice items, credit notes, the balance, payouts, dispute reads, Radar value lists and Connect’s v2 accounts are modelled: acting as a connected account, its capabilities, bank account, balance and payouts, destination charges, and an account with the customer configuration paying as customer_account. Connect’s v1 accounts, the transfer and application fee routes, quotes, tax and dispute updates are not.

Control plane

Every route under /_rystic/ is listed in routes by plan. GET /_rystic/egress holds the webhook events your requests triggered (Egress log), /_rystic/ui shows the account as the Stripe Dashboard (The Dashboard UI), and Stripe has no WebSocket feed, so /_rystic/ws lists none.

Scenarios

design_studio is the one shipped world: Website Design Contractors’ own account on 2026-03-01T00:00:00Z. 30 clients as customers (5 paying by US bank account), 22 billed monthly off a saved method; 269 charges since November 2024, each intent naming its invoice, with one card declined and paid on a replacement, one refunded in full, one disputed and one bank debit still pending; 7 unpaid invoices as open intents; and the invoice tracker’s webhook endpoint. The same clients, invoice numbers and dates appear in the studio’s Slack, Resend, PayPal and Lob worlds.

curl -s 'localhost:8080/v1/charges?limit=1' -u sk_test_twin:
# -> "description":"Invoice WDC-1328: Hosting, renewal", "metadata":{"invoice":"WDC-1328"}

Worlds covers listing and loading worlds.

Seeding state

One register per resource: state_balance, state_billing_alert, state_billing_credit_balance_transaction, state_billing_credit_grant, state_billing_meter, state_billing_meter_event, state_billing_meter_event_adjustment, state_billing_portal_configuration, state_billing_portal_session, state_charge, state_checkout_session, state_checkout_session_line_item, state_credit_note, state_credit_note_line_item, state_customer, state_discount, state_dispute, state_entitlements_feature, state_event, state_event_destination, state_invoice, state_invoice_line_item, state_invoice_payment, state_invoiceitem, state_payment_intent, state_payment_intent_amount_details_line_item, state_payment_link, state_payment_link_line_item, state_payment_method, state_payout, state_price, state_product, state_product_feature, state_radar_value_list, state_radar_value_list_item, state_refund, state_setup_attempt, state_setup_intent, state_subscription, state_subscription_item, state_test_clock, state_v2_core_event, state_webhook_endpoint. Worlds covers seed files, POST /_rystic/seed, GET /_rystic/registers and the seed gate. A boot file with one customer, a pinned clock and a fixed RNG:

{
  "seed": {
    "state_customer": { "cus_dev": { "id": "cus_dev", "object": "customer",
      "email": "dev@example.com" } }
  },
  "clock": "2026-07-23T00:00:00Z",
  "rng_seed": 42
}

Determinism & reset

Ids are sequences that restart on reset, and timestamps come from the twin’s clock: an empty boot starts at 2026-01-01T00:00:00Z, design_studio at 2026-03-01T00:00:00Z, and each request that stamps a time (a write) moves it one millisecond, never by wall time. The same requests from the same reset give byte-identical records, ids, timestamps and events. Reset and determinism covers reset, the clock and the RNG.

Egress log

Every event your requests trigger renders into one ordered log, and GET /_rystic/egress returns it. This is the webhook surface. Boot the twin with RYSTIC_WEBHOOK_DELIVERY=on (--env RYSTIC_WEBHOOK_DELIVERY=on) and each event is also POSTed to every enabled webhook endpoint you registered whose enabled_events names it or *, signed with that endpoint’s secret in Stripe-Signature, and to every enabled thin event destination whose enabled_events names its type, v1.-prefixed for a v1 event, as a thin notification signed with the destination’s signing_secret; without it nothing is POSTed. A v2 account event (v2.core.account.created, .updated, .closed, and each [part].updated and capability_status_updated one) has no v1 snapshot and reaches no webhook endpoint, but a thin destination whose enabled_events names it receives it, and stripe-node’s parseEventNotification, fetchEvent and fetchRelatedObject work against it. An event caused while acting as a connected account is that account’s (Limitation 1), and only connect=true endpoints take it. The signature’s t= is wall-clock time, so stripe.webhooks.constructEvent accepts it however the twin’s clock is pinned, while the event’s own created stays on the twin’s clock. A failed delivery is retried as Limitation 1 describes, each retry signed again as it is sent. Webhooks covers delivery and Observing the log.

{"dropped":0,"events":[{"seq":1,"effect":"eff_evt_customer_created","type":"customer.created",
  "payload":{"created":1767225600,
             "data":{"object":{...,"email":"dev@example.com","id":"cus_lllllllllllllc",...,"object":"customer",...}},
             "id":"evt_lllllllllllllc","object":"event","type":"customer.created"}}],
 "total":1}

The routes below emit, in this order; the table is not exhaustive (subscriptions, invoices, credit notes, test clocks and v2 accounts emit too), and the egress log carries every route’s events:

RequestEvents
POST /v1/customerspayment_method.attached when payment_method attaches one, then customer.created
DELETE /v1/customers/{customer}customer.deleted, then customer.subscription.deleted for each subscription it cancels
POST /v1/chargescharge.succeeded
POST /v1/charges/{charge}charge.updated
POST /v1/payment_intentspayment_intent.created; with confirm=true, then charge.succeeded, and payment_intent.succeeded unless capture_method=manual
POST /v1/payment_intents/{intent}/confirmcharge.succeeded, then payment_method.attached when it attaches its PaymentMethod to the customer, and payment_intent.succeeded unless capture_method=manual; when the PaymentIntent pays an open invoice, then invoice_payment.paid, invoice.updated, invoice.paid, invoice.payment_succeeded, and customer.subscription.updated when that activates an incomplete subscription
POST /v1/payment_intents/{intent}/capturecharge.captured, payment_intent.succeeded
POST /v1/payment_intents/{intent}/cancelpayment_intent.canceled
POST /v1/refundsrefund.created, charge.refunded, refund.updated, charge.refund.updated
POST /v1/refunds/{refund}refund.updated, charge.refund.updated
POST /v1/setup_intentssetup_intent.created
POST /v1/setup_intents/{intent}/confirmsetup_intent.succeeded
POST /v1/setup_intents/{intent}/cancelsetup_intent.canceled
POST /v1/payment_methods/{payment_method}/attachpayment_method.attached
POST /v1/checkout/sessions/{session}/expirecheckout.session.expired
POST /v1/productsproduct.created; with default_price_data, first plan.created (for a recurring default price) and price.created for the price it mints (Stripe’s refusals of a malformed default_price_data are not modelled yet)
POST /v1/products/{id}product.updated, with previous_attributes over the fields it changed
DELETE /v1/products/{id}product.deleted, carrying the product as it stood
POST /v1/pricesplan.created for a recurring price, carrying the legacy Plan view of the price under the same id, then price.created; a create with product_data does not send the product’s product.created yet
POST /v1/prices/{price}plan.updated for a recurring price, then price.updated, each with previous_attributes over the fields it changed

payload is the envelope Stripe would have POSTed, data.object the resource as the API returned it.

The Dashboard UI

Open http://localhost:8080/_rystic/ui in a browser: the Dashboard card draws the twin’s state as the Stripe Dashboard — Home with volume, payment and customer charts, Payments and Refunds, Customers, Payment links, Checkout sessions, and Developers → Webhooks and Events — and redraws as your integration calls the API.

  1. Act as the Dashboard does. Refund all or part of a payment, capture or cancel an intent, add, edit or delete a customer, create or deactivate a payment link, expire a Checkout Session, and add, update, disable or delete a webhook endpoint. Each goes through the twin’s own /v1 API with a placeholder key, so it lands like your integration’s call: it takes the next id and the twin’s clock, renders its events to egress and shows in GET /_rystic/calls. Each dialog’s footer names the call it makes. The twin has one account, so the UI sends no X-Rystic-Actor. Amounts must be positive with no more fractional digits than the currency supports.
  2. Find anything. The search bar jumps to any object id and matches descriptions, emails and amounts. Every detail page opens a JSON drawer with the raw records. Customer spend, refunds and disputes are shown separately for each currency; currencies are never added together.
  3. Events. The Events page lists the seeded /v1/events records and the events your requests caused (/_rystic/egress) side by side, marked by source. Endpoint pages match the newest 300 retained events against current subscriptions, including events from before the endpoint existed. These matches change with endpoint settings and are not delivery history; see Egress log for what is delivered.
  4. Your integration lists the live API calls, Webhook egress lists the rendered events and the endpoints that subscribe to each, and Clock shows and moves the twin’s clock and resets or loads a world.
  5. What the page does not run — balances, payouts, disputes (it shows a disputed charge’s flag), products, billing, receipts and hosted payment pages — is drawn but inert: a click says so. The page contacts nothing but the twin.
  6. Times are UTC, and “today” is the twin’s clock. A reset or scenario load closes open dialogs and detail pages; a reply from the previous world cannot change the new one.

Limitations

  1. Webhook delivery is opt-in. Endpoint CRUD is modelled, and the events your requests trigger render to the ordered egress log; they reach your endpoints only under RYSTIC_WEBHOOK_DELIVERY=on. A delivery your endpoint does not answer with a 2xx within 5 s 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; each retry carries a new Stripe-Signature made as it is sent, and none goes to an endpoint or destination deleted or disabled by then. Stripe documents three sandbox retries over a few hours but not their delays, so these are assumed. Each event is also listed by GET /v1/events and GET /v1/events/{id} under the id its delivery carries, beside whatever a seed put in state_event, and by GET /v2/core/events and GET /v2/core/events/{id} as a v2.core.event under its own evt_test_ id. A request’s events keep the twin clock’s created, and an event the clock sends (a subscription’s renewal, trial or cancel_at, a Checkout Session’s expiry, a pending refund’s settlement) carries the instant it was due, however far a /_rystic/clock move jumps past it; one a test clock’s advance sends carries the twin clock’s time at the request that completes the advance, as Stripe’s carry real time. A v1 event’s pending_webhooks counts the enabled webhook endpoints subscribed to it when it was logged, less each that has taken it with a 2xx (thin destinations count nothing, and a failed attempt lowers nothing), so delivery_success=false lists an event still waiting on a retry or whose retries all failed; with delivery off, nothing lowers it. An event a request causes while acting as a connected account (Stripe-Account or Stripe-Context) is that account’s: its v1 snapshot carries account and context naming it, its v2 rendering context, and the event lists are its own, as is every event the subscription clock sends for a subscription created acting as an account (Limitation 10) and every event a pending refund’s settlement sends for a refund made acting as one. The events of the platform’s own requests are the platform’s and name no account, and so are test_helpers.test_clock.ready and checkout.session.expired, even for a test clock or Checkout Session made acting as an account; which account Stripe gives those two is not yet measured. A clock act for the platform stays the platform’s when a connected account’s request brings the clock to it, such as the request that completes a test clock’s advance. A webhook endpoint created with connect=true takes only connected accounts’ events, and every other endpoint only the platform’s. A destination charge’s py_ payment emits payment.created as the connected account’s, with request {id: null, idempotency_key: null}: it lists among the account’s events and reaches the platform’s Connect endpoints, never the platform’s own list; Stripe lists it about 3 s after the charge, the twin at once. A webhook endpoint at localhost is accepted, where Stripe refuses it with “URL must be publicly accessible”; an event destination there is refused as Stripe refuses it.
  2. Little moves on its own. Drive time via /_rystic/clock. The clock settles a sepa_debit payment and a verified us_bank_account one (Limitation 11), and moves a subscription’s trial, renewal, draft invoices and resume expiry (Limitation 10), a subscription reaching its cancel_at, which cancel_at_period_end=true sets to the period end (Limitation 8), and a Checkout Session reaching its expires_at, 24 hours after its create unless the create sets it (an integer from 30 minutes to 24 hours ahead; a seeded session with none reads a day after its created), when an open session reads expired and checkout.session.expired goes out, stamped with that expires_at when a /_rystic/clock move reaches it. A connected seller’s capabilities activate on it too. A seller created with its onboarding details (an individual identity with date of birth, address, ID number and terms-of-service acceptance, and a business URL) reads card_payments pending until 85 seconds after its create, then active. Once it has an external account, its payouts read pending from 18 seconds and active from 85. Each capability whose status moves sends its capability_status_updated event, as Stripe’s activation does, and a card_payments re-requested after Stripe unfreezes it reads active at once on a seller past its activation; on one frozen before it, card_payments reads pending again, and payouts too while it holds a bank, until the seller’s own due activates them. Until activation, a charge as the seller answers “Your account cannot currently make charges.” Advance the clock past it with POST /_rystic/clock {"advance_seconds": 85}, or run with RYSTIC_PACE=1 so the clock follows real time. A refund of a captured pm_card_pendingRefund / tok_pendingRefund charge, on any create route or through a credit note’s refund_amount, answers pending with pending_reason insufficient_funds and a card destination naming no acquirer reference yet; once the clock reaches 122 seconds after its created it reads succeeded, and at 249 seconds its acquirer reference lands, each move sending refund.updated and charge.refund.updated. Stripe’s own instants vary by a few seconds. A legacy POST /v1/charges/{charge}/refund refund of a charge whose balance transaction has not attached yet (only a seeded charge, as the twin attaches one at its confirm) is succeeded at once with no balance transaction, and on the pendingRefund card its reference stays pending. A test clock’s advance completes by itself: the next API request still reads the clock advancing with its old frozen_time (an advance sent then answers 429, as Stripe’s does), and the request after it reads the clock ready at the target, having moved the customers and subscriptions on the clock to it (Limitation 10). A request counts once the twin hands it to an operation, a hosted page or /_dispute, even when that handler refuses it (a 401 without a key, say), and so does a world timeline call to an API route when it fires (a sim-local one such as /_dispute does not). A request the twin answers before that (an unserved path or method, an admin-token refusal, an oversized body, a /_rystic/network reject), a /_rystic/ control call (seed, clock, reset, inspect, version, or an unknown /_rystic/ path), a clock jump or a paced tick (RYSTIC_PACE) that fires no timeline call, and a read of the state UI, its activity and events streams, the explorers or a file URL leave the clock advancing. An advance takes frozen_time as an integer as sent (+<t> reads as <t>; a space-prefixed, empty, array or hash value answers 400 parameter_invalid_integer, the value echoed as Stripe renders it, {:a=>"<t>"} for frozen_time[a]), from 2000-01-01 to 2100-01-01, not before the clock’s time (its own time is accepted) and at most two of the shortest recurring interval of the clock’s live subscriptions on: two calendar months for a monthly one, six years on a clock holding none. A clock still advancing answers its 429 before frozen_time is read. A clock create takes the same integers and range, and answers a negative time 400 “Invalid timestamp: must be an integer”.
  3. The balance moves with card payments, refunds and payouts. Every account starts with a usd entry at zero. A connected account’s balance also lists instant_available beside available, and the platform’s lists connect_reserved at zero. 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 (card 4000000000000077, also when saved as a PaymentMethod) and to pending for every other card and for a sepa_debit payment once the clock settles it (Limitation 11); a verified us_bank_account payment adds its amount less Stripe’s ACH fee to available once the clock settles it; pending funds never become available on the twin’s clock. A destination charge (a PaymentIntent with transfer_data[destination], paid at create, confirm or capture) credits the platform its net as any card payment does, then transfers transfer_data[amount] (capped at the captured amount) or else the whole amount to the connected account, which gains it less any application_fee_amount; sending both answers 400 parameters_exclusive, as Stripe does. The platform lists a transfer balance transaction for minus the share and, with a fee, an application_fee one for plus it; the account lists a payment one whose source is the py_ payment it reads as a Charge. The twin moves both at the charge, where Stripe shows them seconds later. A refund with reverse_transfer=true takes its proportion of the share back from the account (a trr_ reversal the refund’s transfer_reversal names, and a pyr_ refund of the payment), and refund_application_fee=true returns its proportion of the fee, each rounded half up on its own, the refund that leaves the transfer fully reversed returning the rest of the fee; Stripe refuses a reversal that rounds to 0 and a fee refund without reverse_transfer, which the twin accepts; without reverse_transfer the platform alone bears the refund, and Stripe’s reserve_transaction rows are not modelled. A refund takes its amount back out; an invoice paid by card credits the balance as a card payment does, and a credit note’s refund_amount debits it as a refund does. A payout draws on available or answers balance_insufficient; the create answers it pending and every later read finds it paid, as test mode pays a payout at once, so a cancel answers 400 “Payouts can only be canceled while they are pending.” A payout writes a payout balance transaction for minus its amount, the one its balance_transaction names, and a cancel that succeeds a payout_cancel one; a connected account’s payout names as destination the bank account its external account create returned, and arrival_date is midnight UTC of the payout’s day. Disputes do not move the balance, and a charge in another currency is credited in that currency rather than converted to usd.
  4. A dispute test card opens a chargeback; nothing moves it after. A captured charge paid with tok_createDispute (POST /v1/charges) or pm_card_createDispute (a PaymentIntent created with confirm=true, or confirmed) opens a needs_response Dispute at once: 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. The charge create itself answers disputed: false, as Stripe does before the chargeback lands, and a charge with no dispute answers its dispute read with 404 “No dispute for charge”. No API call updates or closes a Dispute. A world can also open one: design_studio holds a disputed charge with its Dispute, and POST /_dispute {"charge": "ch_…", "reason"?, "amount"?} on a captured charge stores the same Dispute, marks the charge and sends charge.dispute.created to the egress log and your registered endpoints. A world timeline schedules it as a sim-local call. Otherwise it is a control route like /_rystic/*: open by default, and once the twin boots with RYSTIC_ADMIN_TOKEN set, a request needs that value in an X-Rystic-Admin-Token header. It answers 400 without charge, for an unknown reason or for an amount outside 1 to the amount captured, 404 for an unknown charge, and 409 for one that took no funds or is already disputed. Refunding a disputed charge answers Stripe’s 400 charge_disputed.
  5. Test-mode semantics. Converged against api.stripe.com test mode; live-only behavior is out of scope.
  6. One process = one account world. Run multiple ports for isolation.
  7. Hosted Checkout completes subscriptions only. A Checkout Session’s url opens the twin’s own page at /c/pay/{session} on the origin you called, with the line items, total, prefilled email and a link back to cancel_url. Subscribing on a subscription-mode session creates the customer and a subscription carrying the session’s prices, quantities and subscription_data[metadata], completes the session and redirects to success_url, emitting customer.created, customer.subscription.created and checkout.session.completed in that order; any card number is accepted. Submitting a payment or setup session answers 501 and the session stays open.
  8. The customer portal changes seats and cancels. A portal session’s url opens the twin’s own portal at /p/session?secret=… on the origin you called, listing the customer’s subscriptions with their seats and renewal date. Changing seats, cancelling (which sets cancel_at to the period end and leaves cancel_at_period_end false, as Stripe’s portal does) and taking a cancellation back are each a subscription update that emits customer.subscription.updated to your registered endpoints. Once the clock reaches a subscription’s cancel_at, it reads canceled and customer.subscription.deleted goes out. The portal shows no payment method, billing details or invoice history.
  9. 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, never on invoice.stripe.com, pay.stripe.com or buy.stripe.com. The twin serves no page at those paths: they answer 404.
  10. A subscription bills its first invoice at create, and its trial and renewals run on the clock. POST /v1/subscriptions writes the first invoice and latest_invoice retrieves it. With charge_automatically and a card (the subscription’s default_payment_method, else the customer’s invoice_settings.default_payment_method), the invoice is paid at once by a PaymentIntent and charge, the charge credits the balance, and the create sends payment_intent.created, charge.succeeded, payment_intent.succeeded, customer.updated (only when the customer had no currency yet or a balance the invoice spends, Limitation 12), invoice.finalized, invoice.created, invoice_payment.paid, invoice.paid, invoice.payment_succeeded and customer.subscription.created, in that order, its payment_intent.created carrying the PaymentIntent as created, before the charge. A charge_automatically create with no card and no trial answers Stripe’s 400 resource_missing once the world holds any PaymentMethod; in a world that holds none it reads active with an open first invoice. A send_invoice subscription’s first invoice is a draft that finalizes an hour after its create. A billing_cycle_anchor ahead of a create with no trial runs the first period to the anchor, billed as a proration of the interval that ends there. A trial (trial_period_days, or a trial_end after the create’s clock, its customer’s test clock’s frozen_time when it is on one) needs no card: its first invoice is paid at zero with lines reading Free trial for <quantity> × <product>, its items run to trial_end, its billing_cycle_anchor is trial_end unless the create sets one, and a customer with no currency takes the subscription’s. With no card, a charge_automatically trial also mints a pending SetupIntent, requires_payment_method with usage off_session, named in pending_setup_intent and announced by setup_intent.created before the create’s other events; nothing later clears it. A send_invoice trial is unmeasured and keeps pending_setup_intent null, and the succeeded SetupIntent Stripe makes for a trial with a card is not modelled. A create answers Stripe’s 400 to trial_end beside trial_period_days, and to a trial_end at or before its clock with param trial_end. A create’s cancel_at records a cancellation request, canceled_at the create’s time; one before the first period’s end ends that period there, billed as a proration (a trial keeps its trial_end and anchor, its zero line running to cancel_at), and the clock ends the subscription at cancel_at with no further invoice; min_period_end, max_period_end or max_billed_until sets it to the first period’s end, billing that period whole; one at or before the create’s clock, or beside cancel_at_period_end=true, answers Stripe’s 400. Before it looks anything up, a create also answers Stripe’s 400 to, in this order, an empty collection_method (parameter_invalid_empty) or one other than charge_automatically or send_invoice, an empty trial_end or one that is neither now nor a unix time, and an empty cancel_at or one that is neither a unix time nor min_period_end, max_period_end or max_billed_until, each naming only that parameter even beside one it conflicts with. A later trial_end on an update moves the anchor, the period end and a period-end cancel_at with it and bills the added days at zero; one at or before the subscription’s clock answers Stripe’s 400 with param trial_end, and one that is neither now nor a unix time answers 400 “Invalid trial_end: must be one of now”, as the twin answers a hash or an array too (Stripe’s answer to those is unmeasured). An update whose items[i][id] is empty answers 400 parameter_invalid_empty. Seven days before a renewal’s period end, invoice.upcoming goes out naming no request, once per period end, previewing the renewal invoice; the lead is the account’s “upcoming renewal events” setting, which the twin does not model and fixes at seven days, and a subscription set to cancel at its period end, or a period shorter than seven days, gets none. Three days before trial_end (at once for a shorter trial, and on a test clock at its next advance) the clock sends customer.subscription.trial_will_end, after invoice.upcoming when the trial’s end will bill and the trial runs three days or more. At trial_end a subscription with a card, a send_invoice one, or one whose trial_settings[end_behavior][missing_payment_method] is create_invoice (the default) reads active and renews, billing a proration up to a billing_cycle_anchor after trial_end first; with no card under charge_automatically, or with pm_card_chargeCustomerFail (a test card that attaches but whose every charge is declined), the invoice’s payment fails as it finalizes an hour later, so it stays open with next_payment_attempt 42.1 h on, and the subscription reads past_due and the customer delinquent. The clock retries at next_payment_attempt: with no card, or the declining one (inferred), the attempt fails again and the next is dated 42.1 h on, until the ninth attempt, which leaves the invoice open with no retry, cancels the subscription for payment_failed and keeps the customer delinquent; with a default card that charges, the retry pays the invoice. Voiding, marking uncollectible or paying the invoice reads the subscription active in that request, and the customer not delinquent even while another of its subscriptions is still past_due. Ending a subscription, by DELETE, at its cancel_at, at its last failed retry or by its customer’s delete, stops automatic collection of every open invoice of the customer and of the subscription’s renewal draft, with invoice.updated naming no request, so the clock retries none of them and the draft never finalizes; a customer’s or a test clock’s delete marks the draft uncollectible instead, and cancel_at_period_end=true stops nothing before the period ends. DELETE with invoice_now=true and prorate=true, in the body or the query string, credits each item’s unused time on a final 0.00 invoice paid an hour later onto the customer’s balance, clearing delinquent; a cancel_at set inside the period cuts the items’ period there and leaves the credit pending until the clock ends the subscription and bills it the same way, stopping the open invoices once that invoice finalizes. With no card, pause pauses the subscription instead and cancel cancels it. trial_end=now on an update ends a trial and bills the new period at once. When the clock reaches an active or past_due subscription’s period end, a draft renewal invoice is written and finalizes an hour later. Periods are counted from billing_cycle_anchor, each ending on its day of the month or the month’s last day, so a Jan 31 anchor renews on Feb 28, Mar 31 and Apr 30. Each item renews on its own price’s interval, so a yearly item beside a monthly one is billed once a year, and an item added later takes the subscription’s current period; unless proration_behavior is none, one added to an active or past_due subscription leaves a pending proration for the rest of the period that the next renewal bills first (under always_invoice, POST /v1/subscription_items bills it at once instead, on an invoice paid from the card under charge_automatically, paid with no payment when there is nothing to bill, or under send_invoice a draft that finalizes an hour later, and under charge_automatically with a proration above 0 and no stored payment method the add is refused with Stripe’s 400 resource_missing, while an update’s items[] add waits; a price or quantity change prorates nothing; deleting an item mid-period leaves a pending credit for its unused time, figured from what was last billed for it, and writes no invoice). An update whose items[] names another subscription’s item, or an id no item has, answers Stripe’s 400 and changes neither subscription, as does one whose cancel_at is not after the subscription’s clock (a test clock’s frozen_time on one) or that sets cancel_at_period_end, true or false, or billing_cycle_anchor=now on a paused subscription (with both, the anchor’s refusal answers); a cancel_at_period_end that is neither true nor false answers 400 “Invalid boolean: ” before either. A renewal or a trial’s end on the clock whose next period would run past cancel_at bills only up to cancel_at, and a renewal into a period shorter than the price’s interval (after a cleared cancel_at) bills that period, each as a proration. billing_cycle_anchor=now on an update of an active or past_due subscription restarts its period with no invoice and, unless proration_behavior is none, leaves one pending proration for the days the new period adds, which the next renewal bills first; on a paused one it is refused, as above, and on any other it moves the anchor alone. An item whose current_period_end is not a time, as a seeded record’s 0, never renews. Invoice items created with POST /v1/invoiceitems stay pending, a subscription in the request included. A subscription the clock ends at its cancel_at or period end keeps the request’s time in canceled_at, with ended_at the end. A card declining on the clock, as pm_card_chargeCustomerFail does at a trial’s end or a renewal, leaves a failed charge and the PaymentIntent requires_payment_method with the decline as its last_payment_error, sending charge.failed and payment_intent.payment_failed, and paying the invoice with another card clears that last_payment_error; the clock’s payment_intent.created carries the PaymentIntent as created, before its charge. A request charging that card is declined the same way: a PaymentIntent created with confirm=true or confirmed, and an invoice pay, answer Stripe’s 402 card_error (card_declined, generic_decline), and the invoice a pay names stays open with one attempt. A subscription create charging that card, trial_end=now or not, reads incomplete, its first invoice open with one attempt and its PaymentIntent requires_payment_method, and sends payment_intent.created, charge.failed, payment_intent.payment_failed, customer.updated (only when the customer had no currency yet or a balance the invoice spends), invoice.payment_failed, invoice.created, invoice.finalized and customer.subscription.created; under payment_behavior=error_if_incomplete it answers the decline’s 402, leaves no subscription or invoice and cancels the PaymentIntent. Neither makes the customer delinquent. Paying an incomplete subscription’s open invoice, by an invoice pay or by confirming its PaymentIntent with a card that charges, makes it active at once, reusing that PaymentIntent and invoice payment, and sends charge.succeeded, payment_intent.succeeded, invoice_payment.paid, invoice.updated, invoice.paid, invoice.payment_succeeded and customer.subscription.updated, a confirm sending payment_method.attached after the charge as it attaches the card. An update’s trial_end=now charging the declining card is declined the same way, leaving its invoice open with one attempt and the subscription past_due, its customer not made delinquent, and invoice.payment_failed, naming no request, follows the update’s own events. A resume under billing_cycle_anchor=now (the default) leaves the subscription paused with a pending_update and an open invoice for the period it starts, which nothing charges on its own; once that invoice is paid or marked uncollectible the subscription reads active from the resume, sending customer.subscription.resumed, which carries the subscription as it stood before (trialing, as Stripe’s does), then customer.subscription.pending_update_applied and customer.subscription.updated. Once the clock passes pending_update.expires_at, 23 hours after the resume, an unpaid resume invoice is voided, its PaymentIntent canceled and pending_update cleared, the subscription staying paused and sending customer.subscription.pending_update_expired; a DELETE before then does the same before it cancels. A resume under billing_cycle_anchor=unchanged reads active at once with no invoice, on the anchor’s schedule, and unless proration_behavior is none the next renewal also bills the rest of the period it resumed in. A customer created with test_clock lives on that clock: it and its subscriptions read the clock’s frozen_time in created, their periods, trial dates and invoices, each invoice carries the clock’s test_clock, and only the clock’s advance moves them, every act due in it at its own instant on the clock, while their events and the PaymentIntents and charges paying their invoices keep the twin’s clock. /_rystic/clock moves only the subscriptions on no test clock. A subscription created acting as a connected account (Stripe-Account or Stripe-Context) settles as that account: the PaymentIntents and charges its renewals, trial end and later payments make are in that account’s lists and balance, not the platform’s, and every event its clock sends is the account’s. Deleting a customer cancels its live subscriptions at once, at the delete’s time on their clock, so none renews, and marks each one’s draft invoices uncollectible first, never finalized or charged (a void of one answers Stripe’s 400), leaving its pending invoice items pending; deleting a test clock deletes its customers and cancels the subscriptions on it, a deleted customer’s included. Either sends customer.deleted, carrying the customer as it stood, before customer.subscription.deleted; a test clock delete sends test_helpers.test_clock.deleted between them, its customers’ events oldest first and its subscriptions’ newest first. A test clock’s create sends test_helpers.test_clock.created; its advance sends test_helpers.test_clock.advancing at once and, when it completes, every event the completion settles, then test_helpers.test_clock.ready. A customer create whose test_clock names no clock, or a deleted one, answers Stripe’s 400 resource_missing, an empty test_clock 400 parameter_invalid_empty and a hash 400 “Invalid string: …”, and a Checkout Session’s expiry stays on the twin’s clock whatever its customer.
  11. A sepa_debit payment settles on the clock, and a us_bank_account one settles once its microdeposits are verified. A PaymentMethod created with type=sepa_debit and an iban, or one a PaymentIntent’s payment_method_data makes, reads the IBAN’s country, last four and bank code, as Stripe’s does. A PaymentIntent created with confirm=true and a sepa_debit method answers processing with a pending charge, sending payment_intent.created, payment_intent.processing and charge.pending. 15 seconds after the charge, the clock settles it as Stripe’s does: charge.updated as it names its balance transaction, then payment_intent.succeeded and charge.succeeded, and the amount less the card fee moves to pending. The test IBAN DE62370400440532013001 fails instead: the intent returns to requires_payment_method with last_payment_error (payment_intent.payment_failed), the charge reads failed with failure_code incorrect_account_holder_name (charge.failed), then charge.updated, and no balance moves; a charge whose intent was cancelled while processing stays pending, and a payment made acting as a connected account settles the same way in that account, its settle events the account’s. Stripe settles 12 to 16 seconds after the charge and documents no delay; the twin’s fee follows the card’s rule. A us_bank_account PaymentIntent created with confirm=true, a customer and no Financial Connections account answers requires_action with verify_with_microdeposits, the deposits arriving at 07:00 UTC the next day (one capture’s rule) and hosted_verification_url on the origin you called, where the twin serves no page. Verifying the microdeposits takes descriptor_code or amounts, never both or neither (Stripe’s parameters_exclusive and parameter_missing 400s), and answers Stripe’s test values, a code in any case and the amounts in either order: SM11AA, or amounts 32 and 45, answers processing with a pending charge (payment_intent.processing, charge.pending), and 20 seconds after the charge the clock settles it as a sepa_debit one succeeds, its balance transaction Stripe’s ACH payment (type payment, a 0.8% fee, available 5 seconds after it is created); Stripe settled 15 to 39 seconds after the verify. SM33CC, or amounts 10 and 11, answers Stripe’s attempts-exceeded 400 and SM44DD, or amounts 40 and 41, the intent timed out, each returning the intent to requires_payment_method with a failed charge (payment_intent.payment_failed, charge.failed); any other code or amounts answers Stripe’s mismatch 400 with the attempts left, and the tenth wrong verify fails the intent in its own request. A us_bank_account SetupIntent created with confirm=true awaits microdeposits too, and its verify takes the same values: the verifying ones succeed it at once, attaching the PaymentMethod to the customer (payment_method.attached, setup_intent.succeeded), SM33CC and SM44DD leave it requires_payment_method with its PaymentMethod unattached (setup_intent.setup_failed), and a wrong code answers the mismatch 400 with its own ten attempts. A sepa_debit SetupIntent created with confirm=true succeeds at once. The confirm route with these methods, a verified payment failing on the clock, refunding a sepa_debit charge and a SetupIntent’s failing IBAN are not modelled.
  12. A customer’s balance is applied to its next invoice. A customer’s balance, a credit when negative and an amount owed when positive, is set by POST /v1/customers or its update, lowered by a credit note’s credit_amount, or left by an invoice that ends below zero; the twin serves no /v1/customers/{customer}/balance_transactions. A draft invoice reads starting_balance as the customer’s balance when it is created, ending_balance null, and amount_due as total plus starting_balance, never below 0: after a -500 credit, a draft with a 1200 item reads amount_due 700 with total 1200. Finalizing reads the balance again, so a credit another invoice spent first is not applied twice: the invoice reads ending_balance 0, or the rest of a larger credit, its PaymentIntent asks for the reduced amount, the customer’s balance becomes the invoice’s ending_balance, and customer.updated goes out before the finalize’s invoice.updated, as Stripe’s does. A credit that covers the whole invoice leaves it paid at the finalize with no PaymentIntent, and a subscription’s first invoice and its renewals spend the balance the same way. A credit smaller than the invoice is measured; a larger credit, a positive balance and the subscription invoices follow the same rule, inferred. The applied_to_invoice customer balance transaction is not modelled, and voiding an invoice does not give the balance back.

Known issues.

  • A PaymentIntent cancel accepts an unknown cancellation_reason, where Stripe answers 400
  • A test card can’t always be saved: a confirmed SetupIntent saves no card, and pm_card_bypassPending, pm_card_pendingRefund and pm_card_createDispute can’t be attached to a customer
  • A card that attaches but declines every charge declines only as pm_card_chargeCustomerFail, and as tok_chargeCustomerFail on POST /v1/charges: a PaymentMethod made from that token or from its number 4000000000000341 charges successfully, where Stripe declines

Not served. 416 of the 612 operations in the published spec are not served.

  • accounts, 1 of 29 served: GET /v1/accounts, POST /v1/accounts, GET /v1/accounts/{account}, POST /v1/accounts/{account}, DELETE /v1/accounts/{account}, POST /v1/accounts/{account}/bank_accounts, GET /v1/accounts/{account}/bank_accounts/{id}, POST /v1/accounts/{account}/bank_accounts/{id}, … and 20 more
  • balance, 1 of 3 served: GET /v1/balance/history, GET /v1/balance/history/{id}
  • charges, 12 of 14 served: POST /v1/charges/{charge}/dispute, POST /v1/charges/{charge}/dispute/close
  • core, 15 of 25 served: POST /v2/core/account_links, POST /v2/core/account_tokens, GET /v2/core/account_tokens/{id}, POST /v2/core/accounts/{account_id}/person_tokens, GET /v2/core/accounts/{account_id}/person_tokens/{id}, GET /v2/core/accounts/{account_id}/persons, POST /v2/core/accounts/{account_id}/persons, GET /v2/core/accounts/{account_id}/persons/{id}, … and 2 more
  • customers, 8 of 47 served: GET /v1/customers/{customer}/balance_transactions, POST /v1/customers/{customer}/balance_transactions, GET /v1/customers/{customer}/balance_transactions/{transaction}, POST /v1/customers/{customer}/balance_transactions/{transaction}, GET /v1/customers/{customer}/bank_accounts, POST /v1/customers/{customer}/bank_accounts, GET /v1/customers/{customer}/bank_accounts/{id}, POST /v1/customers/{customer}/bank_accounts/{id}, … and 31 more
  • disputes, 2 of 4 served: POST /v1/disputes/{dispute}, POST /v1/disputes/{dispute}/close
  • invoices, 12 of 18 served: POST /v1/invoices/create_preview, POST /v1/invoices/{invoice}/attach_payment, POST /v1/invoices/{invoice}/lines/{line_item_id}, POST /v1/invoices/{invoice}/remove_lines, POST /v1/invoices/{invoice}/send, POST /v1/invoices/{invoice}/update_lines
  • payouts, 5 of 6 served: POST /v1/payouts/{payout}/reverse
  • radar, 9 of 12 served: GET /v1/radar/early_fraud_warnings, GET /v1/radar/early_fraud_warnings/{early_fraud_warning}, POST /v1/radar/payment_evaluations
  • test_helpers, 5 of 44 served: POST /v1/test_helpers/confirmation_tokens, POST /v1/test_helpers/customers/{customer}/fund_cash_balance, POST /v1/test_helpers/issuing/authorizations, POST /v1/test_helpers/issuing/authorizations/{authorization}/capture, POST /v1/test_helpers/issuing/authorizations/{authorization}/expire, POST /v1/test_helpers/issuing/authorizations/{authorization}/finalize_amount, POST /v1/test_helpers/issuing/authorizations/{authorization}/fraud_challenges/respond, POST /v1/test_helpers/issuing/authorizations/{authorization}/increment, … and 31 more
  • Not served at all: account (1), account_links (1), account_sessions (1), apple_pay (4), application_fees (7), apps (4), climate (9), confirmation_tokens (1), country_specs (2), coupons (5), customer_sessions (1), entitlements (6), ephemeral_keys (2), exchange_rates (2), external_accounts (1), file_links (4), files (3), financial_connections (11), forwarding (3), identity (8), … and 27 more

Required fields the model does not declare. The spec requires these in a success response; the model’s response shape leaves them out, so the twin may not return them:

  • DELETE /v1/subscriptions/{subscription_exposed_id} (200): automatic_tax, billing_mode, billing_schedules, discounts, invoice_settings
  • DELETE /v1/subscriptions/{subscription_exposed_id}/discount (200): source, start
  • GET /v1/charges/{charge}/dispute (200): evidence_details/enhanced_eligibility
  • GET /v1/invoices (200): data/*/amount_overpaid, data/*/amount_paid_off_stripe, data/*/amount_shipping, data/*/attempt_count, data/*/attempted, data/*/auto_advance, data/*/automatic_tax, data/*/default_tax_rates, … and 9 more
  • GET /v1/invoices/{invoice} (200): amount_paid_off_stripe
  • GET /v1/payment_links (200): data/*/after_completion, data/*/automatic_tax, data/*/custom_fields, data/*/custom_text, data/*/phone_number_collection, data/*/shipping_options, data/*/tax_id_collection
  • GET /v1/subscriptions (200): data/*/automatic_tax, data/*/billing_mode, data/*/billing_schedules, data/*/discounts, data/*/invoice_settings
  • GET /v1/subscriptions/{subscription_exposed_id} (200): items/data/*/discounts, items/data/*/metadata, items/data/*/price/active, items/data/*/price/billing_scheme, items/data/*/price/livemode, items/data/*/price/metadata, items/data/*/price/type
  • POST /v1/invoices (200): amount_overpaid, amount_paid_off_stripe, attempt_count, attempted, auto_advance, automatic_tax, default_tax_rates, discounts, … and 8 more
  • POST /v1/invoices/{invoice} (200): default_tax_rates, discounts
  • POST /v1/invoices/{invoice}/add_lines (200): amount_overpaid, amount_paid_off_stripe, amount_shipping, attempt_count, attempted, auto_advance, automatic_tax, default_tax_rates, … and 13 more
  • POST /v1/invoices/{invoice}/finalize (200): amount_overpaid, amount_paid_off_stripe, amount_shipping, attempt_count, attempted, auto_advance, automatic_tax, created, … and 11 more
  • POST /v1/invoices/{invoice}/finalize (200): amount_overpaid, amount_paid_off_stripe, amount_shipping, attempt_count, attempted, auto_advance, automatic_tax, collection_method, … and 14 more
  • POST /v1/invoices/{invoice}/mark_uncollectible (200): amount_paid_off_stripe, attempt_count, attempted, auto_advance, automatic_tax, default_tax_rates, discounts, issuer, … and 4 more
  • POST /v1/invoices/{invoice}/pay (200): auto_advance, automatic_tax, default_tax_rates, discounts, issuer, payment_settings, period_end, period_start, … and 3 more
  • POST /v1/invoices/{invoice}/void (200): amount_overpaid, amount_paid_off_stripe, amount_shipping, attempt_count, attempted, auto_advance, automatic_tax, default_tax_rates, … and 8 more
  • POST /v1/payment_links/{payment_link} (200): after_completion, automatic_tax, custom_fields, custom_text, phone_number_collection, shipping_options
  • POST /v1/payouts (200): id
  • POST /v1/subscription_items/{item} (200): price/metadata
  • POST /v1/subscriptions (200): automatic_tax, billing_mode, billing_schedules, discounts, invoice_settings, items/data/*/discounts, items/data/*/metadata, items/data/*/price/billing_scheme, … and 4 more
  • … and 3 more

Not measured. The fidelity suite runs 1432 probes, and 43 documented branches have none. 6 need a credential the measuring account does not hold (200 ×3, 401 ×1, 403 ×2). The other 37:

OperationStatusWhy
DELETE /v1/products/{product}/features/{id}200the model describes no input that reaches it
GET /v1/balance_transactions400the model describes no input that reaches it
GET /v1/billing/credit_balance_summary200the model describes no input that reaches it
GET /v1/billing/credit_balance_summary404the model describes no input that reaches it
GET /v1/credit_notes/preview400nothing the twin serves creates the record it needs
GET /v1/credit_notes/preview/lines400the model describes no input that reaches it
GET /v1/events/{id}200nothing the twin serves creates the record it needs
GET /v1/invoices/{invoice}/lines400nothing the twin serves creates the record it needs
GET /v1/payment_intents/search200the model describes no input that reaches it
GET /v1/prices/search200the model describes no input that reaches it
GET /v1/products/search200the model describes no input that reaches it
GET /v1/products/{product}/features200the model describes no input that reaches it
GET /v1/products/{product}/features/{id}200the model describes no input that reaches it
GET /v1/radar/value_list_items404the model describes no input that reaches it
GET /v1/subscription_items400nothing the twin serves creates the record it needs
GET /v1/subscription_items404the model describes no input that reaches it
GET /v1/subscriptions/search200the model describes no input that reaches it
POST /v1/charges/{charge}/refunds/{refund}200the model describes no input that reaches it
POST /v1/charges/{charge}/refunds/{refund}400the model describes no input that reaches it
POST /v1/charges/{charge}/refunds/{refund}404the model describes no input that reaches it
… and 14 more

2 variant probes of branches the suite measures are not sent and are kept as unmeasured: GET /v1/billing/alerts (200), POST /v1/payouts (200).

Not modeled. What the model’s operations state they leave out:

  • The guard order between the item’s existence check and the draft-attachment check is assumed (existence first) but not measured. (DELETE /v1/invoiceitems/{invoiceitem})
  • Whether the refusal for a non-draft attachment carries an error code (the spec’s default error response names no code for this route). (DELETE /v1/invoiceitems/{invoiceitem})
  • No captured trace of the non-draft refusal body; its status, code and message are inferred from the operation description and the sibling void refusal. (DELETE /v1/invoices/{invoice})
  • Reading the emitted v1.invoice.deleted event back through GET /v2/core/events?object_id= is documented in the operation note (ev_op_delete_v1_invoices_invoice) but is not declared here: object_id is a parameter of the event-list route, not of this DELETE, which takes only the path parameter invoice. (DELETE /v1/invoices/{invoice})
  • The subscription-invoice half of the draft guard: a draft invoice generated for a subscription is also refused, but no in-scope capture distinguishes its message or error code from the finalized-invoice refusal, so both land on br_400_invoice_not_draft. (DELETE /v1/invoices/{invoice})
  • The SKU restriction for type=good products: no SKU resource exists in this slice, so the branch where a product with SKUs is refused is not modeled. (DELETE /v1/products/{id})
  • The price-block guard reads the whole price collection because prices are keyed by their own id, not by product; no member-keyed absence predicate is expressible, so no machine-readable x_expects is declared. (DELETE /v1/products/{id})
  • Whether the 400 price-block error carries an error.code and doc_url is unmeasured; only type and message are asserted in the example. (DELETE /v1/products/{id})
  • Whether an unknown body parameter is rejected (additionalProperties false in the request schema) is declared from the spec but unmeasured on the live service. (DELETE /v1/products/{product}/features/{id})
  • Whether deleting an attachment whose product exists but whose id belongs to a different product answers 404 with param ‘id’ or param ‘product’ is unmeasured. (DELETE /v1/products/{product}/features/{id})
  • Stripe’s refusal when the item’s parent value list is referenced by an active Radar rule is documented on the value-list delete, not on this item delete; no evidence in this bundle states whether an item delete can be refused for that reason. (DELETE /v1/radar/value_list_items/{item})
  • The documented refusal ‘to be deleted, a value list must not be referenced in any rules’ is not modeled: Radar rules are outside this slice, so no state family holds rule references and no derivable recipe exists for that rejection branch. (DELETE /v1/radar/value_lists/{value_list})
  • Stripe refuses deleting the only remaining item of a subscription in practice, but the sliced spec states no such rule or error code; no branch is modeled until wire evidence pins the status/code. (DELETE /v1/subscription_items/{item})
  • clear_usage=true on a non-metered price is documented as disallowed but the refusal’s status and error code are unmeasured. (DELETE /v1/subscription_items/{item})
  • customer.subscription.updated for the parent and the credit’s invoiceitem.created are the subscription reducer’s to emit (asm_subscription_proration), not declared as outbound effects here. (DELETE /v1/subscription_items/{item})
  • proration_date and payment_behavior are not read, and always_invoice credits as create_prorations does, leaving the credit pending (not modelled: Stripe invoices it at once) (asm_subscription_proration). (DELETE /v1/subscription_items/{item})
  • Which non-active statuses (past_due, unpaid, paused, incomplete) the live service accepts for cancel is inferred from the subscription lifecycle, not stated by this operation. (DELETE /v1/subscriptions/{subscription_exposed_id})
  • No in-scope vector creates a Discount (coupons are out of the modelled slice), so the success branch is reachable only against a seeded state_discount record; the probe for br_200_deleted has no API-only producer recipe. (DELETE /v1/subscriptions/{subscription_exposed_id}/discount)
  • Whether removing a subscription discount emits a customer.discount.deleted event is undocumented in this slice; no outbound effect is declared. (DELETE /v1/subscriptions/{subscription_exposed_id}/discount)
  • What a clock delete does to the clock’s invoice items, credit grants and credit balance transactions is unmeasured and not modeled. Its customers are deleted and their subscriptions canceled (asm_test_clock_delete_cascades), and each cancel, its customer already deleted, marks the subscription’s live drafts uncollectible and stops the customer’s open invoices (asm_subscription_cancel_stops_collection; unmeasured for a clock delete). (DELETE /v1/test_helpers/test_clocks/{test_clock})
  • … and 508 more

Cookbook

Charge, refund, assert

curl -s localhost:8080/v1/charges -u sk_test_twin: \
  -d amount=2000 -d currency=usd -d source=tok_visa
# -> {"amount":2000,...,"id":"ch_000000000000000000000001",...}

curl -s localhost:8080/v1/refunds -u sk_test_twin: -d charge=ch_000000000000000000000001

curl -s localhost:8080/v1/charges/ch_000000000000000000000001 -u sk_test_twin:
# -> "refunded": true, "amount_refunded": 2000

curl -s 'localhost:8080/_rystic/egress?tail=5'
# -> charge.succeeded, refund.created, charge.refunded, refund.updated, charge.refund.updated

Authorize now, capture later

curl -s localhost:8080/v1/payment_intents -u sk_test_twin: \
  -d amount=5000 -d currency=usd -d 'payment_method_types[]=card' \
  -d payment_method=pm_card_visa -d capture_method=manual -d confirm=true
# -> "id":"pi_000000000000000000000001", "status":"requires_capture"

curl -s -X POST localhost:8080/v1/payment_intents/pi_000000000000000000000001/capture -u sk_test_twin:
# -> "status":"succeeded", "amount_received":5000

Seed a returning customer

curl -X POST localhost:8080/_rystic/seed -d '{
  "state_customer": { "cus_dev": { "id": "cus_dev", "object": "customer",
    "email": "dev@example.com" } } }'

curl -s localhost:8080/v1/customers/cus_dev -u sk_test_twin:
# -> {...,"email":"dev@example.com","id":"cus_dev",...}

Deterministic ids in CI

curl -X POST localhost:8080/_rystic/reset
curl -X POST localhost:8080/_rystic/clock -d '{"time":"2026-07-23T00:00:00Z"}'
# ... run your payment flow ...
curl -s localhost:8080/_rystic/inspect > state.json
# diff state.json against the last green run: the same requests give the same bytes

Release notes: Changelog

Last updated on