Skip to content

Stripe test clocks: a scripted run for trials, renewals and mid-cycle upgrades (API 2026-08-26.dahlia)

Stripe test clocks are sandbox objects that move simulated time forward, so a subscription's trial end, renewal, proration and payment retries run in minutes. Stripe's documentation, read on 22 September 2026, limits each clock to three customers with three subscriptions each, and one advance to two billing intervals of the shortest subscription.

What a test clock is, and its documented limits

A test clock is a test_helpers.test_clock object in a Stripe sandbox. You set its frozen_time, create customers on it, and move that time forward. Subscriptions, invoices and payment attempts of those customers follow the clock. We read Stripe's test clock guide, the Test Clock object reference and the advance endpoint reference on 22 September 2026; the pages carry no publication date. They document these limits:

LimitDocumented value
Customers per clockThree
Subscriptions per customerThree, including scheduled subscriptions
Quotes not attached to customersTen per clock
Direction of timeForward only after frozen_time is set
One advance with subscriptionsAt most two intervals of the shortest subscription period
One advance without subscriptionsAt most two years
Automatic deletion30 days after creation, per Stripe's test clock guide
Existing customer on a clockNo backdated frozen_time, no Billing Automations on the account, no removal from the clock
New invoices per subscription10 per minute and 20 per day
Quantity updates per subscription200 per hour
Bank debit collectionNot supported during advancement, for example us_bank_account
List endpointsClock objects omitted unless the request filters, for example by customer
Clock status valuesadvancing, ready, internal_failure

The reference defines internal_failure as "Failed to advance time. Future requests to advance time will fail."

Why billing tests need Stripe's time to move

Trial conversion, renewal, proration and dunning are triggered by dates, and the money defects sit at those dates: a renewal invoice left in draft, a customer.subscription.trial_will_end event the product ignores, or a proration credit applied to the wrong invoice. Stripe's guide names two timings a test has to cross. A subscription invoice stays in draft for about one hour before finalisation, and Stripe sends trial_will_end three days before the trial ends. Mocking the clock inside your own code skips both, because Stripe's side of the lifecycle never moves. A test clock moves Stripe's side, so assertions run against the invoices and events production code will receive. On subscription billing platforms (see the billing segment page), we run these tests in staging before each release under subscription billing testing.

One scripted run: trial, renewal and a mid-cycle upgrade

The script below is written for Stripe API version 2026-08-26.dahlia, named as current on Stripe's versioning page on 22 September 2026, and pins it with the Stripe-Version header on every call. It uses bash, curl, jq 1.6 or later, and Stripe CLI v1.51.1, the latest stable stripe-cli release on GitHub, published 21 September 2026. The sandbox needs two monthly USD prices: PRICE_BASIC at 20.00 and PRICE_PRO at 40.00. The customer pays with pm_card_visa, a test payment method from Stripe's testing page, so no card number or cardholder data enters the suite. All data is synthetic.

Timeline in UTC: the clock starts on 1 January 2027, the 7-day trial ends on 8 January, the customer upgrades on 23 January with 16 of 31 days left, and the first paid renewal falls on 8 February.

1. Capture events

stripe listen receives sandbox events without a webhook endpoint in the Dashboard.

Shell
stripe listen --format json \
  --events test_helpers.test_clock.ready,customer.subscription.trial_will_end,customer.subscription.updated,invoice.created,invoice.finalized,invoice.paid,invoice.payment_failed \
  > events.json &

2. Create the clock, the customer and the subscription

Shell
export SK=sk_test_REPLACE_ME PRICE_BASIC=price_REPLACE_ME PRICE_PRO=price_REPLACE_ME
V="Stripe-Version: 2026-08-26.dahlia"
API=https://api.stripe.com/v1

CLOCK=$(curl -s $API/test_helpers/test_clocks -u "$SK:" -H "$V" \
  -d frozen_time=1798761600 -d name=trial-renewal-upgrade | jq -r .id)

CUS=$(curl -s $API/customers -u "$SK:" -H "$V" -d test_clock=$CLOCK \
  --data-urlencode email=clock-01@example.com \
  -d payment_method=pm_card_visa \
  -d "invoice_settings[default_payment_method]=pm_card_visa" | jq -r .id)

SUB=$(curl -s $API/subscriptions -u "$SK:" -H "$V" -d customer=$CUS \
  -d "items[0][price]=$PRICE_BASIC" -d trial_end=1799366400 | jq -r .id)

The subscription joins the clock through the customer, so it takes no clock parameter.

3. Advance and wait for the clock

Shell
advance() {
  r=$(curl -s $API/test_helpers/test_clocks/$CLOCK/advance -u "$SK:" -H "$V" -d frozen_time=$1)
  [ "$(echo "$r" | jq -r .status)" = advancing ] || { echo "$r" >&2; return 1; }
  while :; do
    c=$(curl -s $API/test_helpers/test_clocks/$CLOCK -u "$SK:" -H "$V")
    case $(echo "$c" | jq -r .status) in
      ready) [ "$(echo "$c" | jq -r .frozen_time)" = "$1" ] && return 0
             echo "clock stopped at the wrong time" >&2; return 1 ;;
      internal_failure) echo "clock $CLOCK failed" >&2; return 1 ;;
    esac
    sleep 2
  done
}

advance 1799193600 || exit 1  # 6 Jan 2027 00:00: trial_will_end is due
advance 1799373600 || exit 1  # 8 Jan 2027 02:00: trial over, first invoice past the draft hour

4. Upgrade mid-cycle and cross the renewal

Shell
advance 1800662400 || exit 1  # 23 Jan 2027 00:00: 16 of 31 days left
ITEM=$(curl -s $API/subscriptions/$SUB -u "$SK:" -H "$V" | jq -r '.items.data[0].id')
curl -s $API/subscriptions/$SUB -u "$SK:" -H "$V" -d "items[0][id]=$ITEM" \
  -d "items[0][price]=$PRICE_PRO" -d proration_behavior=create_prorations > /dev/null
advance 1802052000 || exit 1  # 8 Feb 2027 02:00: renewal invoice past the draft hour

5. Read the invoices and the events

Stripe's test clock guide states that GET /v1/invoices returns clock invoices only with a filter, so the request filters by customer. On this API version the proration flag sits under the line's parent, per the Invoice Line Item object.

Shell
curl -s -G $API/invoices -u "$SK:" -H "$V" -d customer=$CUS -d limit=10 \
  | jq -r '.data | reverse[] | [.status, .amount_paid,
      ([.lines.data[] | select((.parent.subscription_item_details.proration
        // .parent.invoice_item_details.proration) == true) | .amount]
       | sort | map(tostring) | join(" "))] | @tsv'

Our reference model expects three paid invoices: the 0.00 trial invoice, 20.00 on 8 January, and 50.33 on 8 February, which is 40.00 plus a charge of 20.65 and a credit of 10.32 for 16 of 31 days.

Output
paid	0
paid	2000
paid	5033	-1032 2065

A different pair of proration amounts is a finding; the prorated billing testing guide holds the model that tells which side is wrong. Next, count the customer's events:

Shell
jq -s --arg c "$CUS" '[.[] | select(.data.object.customer == $c) | .type]
  | group_by(.) | map({(.[0]): length}) | add' events.json

The run passes when customer.subscription.trial_will_end appears once and invoice.paid three times. stripe listen renders payloads in the account's default API version unless --latest is set, so the filter reads only type and data.object.customer.

6. Delete the clock

Shell
curl -s -X DELETE $API/test_helpers/test_clocks/$CLOCK -u "$SK:" -H "$V"

Stripe's guide states that deleting the clock deletes its customers and cancels their subscriptions.

How the limits shape a test suite

These are our practices, derived from the documented limits above.

  • One clock per scenario. A clock holds three customers, and every customer on it moves together. Parallel scenarios each get their own clock, named after the test case.
  • A filter on every read, and a non-empty assertion. GET /v1/invoices without a filter omits clock invoices. A reconciliation check that lists all invoices and compares totals sees an empty set and passes. We filter by customer or subscription and fail the test when the list is empty.
  • Check the advance response, then the time. The advance endpoint returns an error when frozen_time exceeds two intervals. A loop that only polls for ready then passes at the old time, which is why step 3 checks for advancing and compares frozen_time.
  • Advance a few minutes between bursts. Requests on a clock subscription all count toward the same frozen moment. Stripe's guide caps new invoices at 10 per subscription per minute and advises advancing the clock a few minutes before more requests.
  • Wait for events on bank debits. A us_bank_account payment is collected after the advance, and the subscription stays active until then. ACH retry cases wait for invoice.payment_failed and customer.subscription.deleted; the card retry schedules in the dunning testing guide settle within the advance.
  • Read deletes_after from each clock. The guide states 30 days, while the object reference example shows deletes_after 604,800 seconds (seven days) after created. Shared fixtures on a clock can disappear between runs.
  • Treat internal_failure as terminal. Stripe states that later advance requests will fail, so the test fails and the next run creates a new clock.

Scope your testing

Send FintechAssuranceLabs your product, segment, money flows, and the standards that need evidence, and get a proposed test scope with its deliverables.

Scope your testing