Skip to Content
SimulatorsStripeTest clocks

Simulators

Test clocks

A test clock gives a group of customers and their subscriptions a time of their own, so you can move one billing scenario through months without moving the rest of the account. Put a subscribed customer on a clock, advance it past the first period and watch the advance complete:

curl -s "$RYSTIC_URL/v1/test_helpers/test_clocks" -u sk_test_123: -d frozen_time=1772323200 | jq -c '{id, status}'
# -> {"id":"clock_1Rystic00000000000000001","status":"ready"}
curl -s "$RYSTIC_URL/v1/customers" -u sk_test_123: -d test_clock=clock_1Rystic00000000000000001 -d payment_method=pm_card_visa -d 'invoice_settings[default_payment_method]=pm_card_visa' | jq -c '{id, test_clock}'
# -> {"id":"cus_lllllllllllllc","test_clock":"clock_1Rystic00000000000000001"}
curl -s "$RYSTIC_URL/v1/prices" -u sk_test_123: -d unit_amount=1500 -d currency=usd -d 'recurring[interval]=month' -d 'product_data[name]=Hosting' | jq -c '{id}'
# -> {"id":"price_1"}
curl -s "$RYSTIC_URL/v1/subscriptions" -u sk_test_123: -d customer=cus_lllllllllllllc -d 'items[0][price]=price_1' | jq -c '{id, status, test_clock}'
# -> {"id":"sub_Rystic0000000001","status":"active","test_clock":"clock_1Rystic00000000000000001"}
curl -s "$RYSTIC_URL/v1/test_helpers/test_clocks/clock_1Rystic00000000000000001/advance" -u sk_test_123: -d frozen_time=1775088000 | jq -c '{status, frozen_time}'
# -> {"status":"advancing","frozen_time":1772323200}
curl -s "$RYSTIC_URL/v1/test_helpers/test_clocks/clock_1Rystic00000000000000001" -u sk_test_123: | jq -c '{status, frozen_time}'
# -> {"status":"advancing","frozen_time":1772323200}
curl -s "$RYSTIC_URL/v1/test_helpers/test_clocks/clock_1Rystic00000000000000001" -u sk_test_123: | jq -c '{status, frozen_time}'
# -> {"status":"ready","frozen_time":1775088000}
curl -s "$RYSTIC_URL/v1/invoices?customer=cus_lllllllllllllc" -u sk_test_123: | jq -c '[.data[].billing_reason]'
# -> ["subscription_cycle","subscription_create"]

What lives on a clock

A customer created with test_clock lives on that clock’s frozen_time: its created and its subscriptions’ created, start_date, periods, trial dates, invoices and invoice payments read it. A subscription and its invoices carry the clock’s test_clock, and an update, cancel or trial_end=now on one stamps the clock’s time. The events they send keep the simulator’s own clock, and the PaymentIntents and charges paying their invoices carry no test_clock and the simulator’s time, as Stripe’s do. A Checkout Session for a customer on a test clock still expires on the simulator’s clock (inferred).

POST /_rystic/clock moves only the subscriptions on no test clock, and one clock’s advance moves only its own customers and subscriptions.

The advance

An advance completes by itself. The next API request still reads the clock advancing with its old frozen_time, and an advance sent then answers 429 “Test clock advancement underway”, as Stripe’s does. The request after it reads the clock ready at the target, with status_details {}, having moved the clock’s customers and subscriptions to it: every renewal, trial warning and end, draft finalize and cancel_at due between the old and new frozen_time happens at its own instant on the clock, while its events carry the time of the request that completes the advance.

A request counts toward completing an advance once the simulator hands it to an operation, a hosted page or /_dispute, even when that handler refuses it (a 404 for an unknown id, say), and so does a scenario timeline call to an API route when it fires. These leave the clock advancing: a request the simulator answers before that (an unserved path or method, a request with no key, an admin-token refusal, an oversized body, a network-profile reject), a /_rystic/ control call, a clock jump or paced tick that fires no timeline call, a timeline call to /_dispute and a read of the Dashboard, its streams, the explorers or a file URL.

frozen_time is an integer as sent: +<t> reads as <t>, and a space-prefixed, empty, array or hash value answers 400 parameter_invalid_integer, echoing the value as Stripe renders it ({:a=>"<t>"} for frozen_time[a]). It runs from 2000-01-01 to 2100-01-01, not before the clock’s time (its own time is accepted and completes there) and at most two of the shortest recurring interval of the clock’s live subscriptions ahead: 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”.

Events

A clock’s create sends test_helpers.test_clock.created, carrying the clock. 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, naming no request. test_helpers.test_clock.ready is the platform’s even for a clock made acting as a connected account, and a clock act for the platform stays the platform’s when a connected account’s request completes the advance.

Deleting a clock

Deleting a test clock deletes its customers as a customer delete does and cancels every live subscription on it at the clock’s time, those of a customer already deleted included (Subscriptions and invoices). It sends each customer’s customer.deleted, oldest first, then test_helpers.test_clock.deleted, then each subscription’s customer.subscription.deleted, newest first. Each subscription’s renewal draft reads uncollectible, with invoice.updated and invoice.marked_uncollectible. What a delete does to the clock’s invoice items, credit grants and credit balance transactions is not modeled.

A customer create whose test_clock names no clock, or a deleted one, answers Stripe’s 400 resource_missing on test_clock; an empty test_clock answers 400 parameter_invalid_empty, and a hash 400 “Invalid string: …”.

Last updated on