Test Clocks
A test clock simulates the passage of time for a chosen set of sandbox
customers. Advance the clock and the billing engine evaluates everything
scheduled for those customers as if the clock’s new frozen_time were now —
plan renewals, subscription rollovers, dunning retries, payment-link expiry.
A month of billing takes seconds instead of a month.
Test clocks are sandbox-only. They never touch live customers or move real money, and requests with a live key are rejected.
How it works
- Create a clock with a starting
frozen_time(the simulated “now”) - Create customers attached to the clock — everywhere the billing engine asks “what time is it?”, these customers get the clock’s time
- Attach billing objects to those customers: payment plans, subscriptions, payment links
- Advance the clock to a new
frozen_time— the advance runs asynchronously - Observe the effects — the
test_clock.readywebhook reports what fired, and the affected objects are in their post-advance states - Repeat as needed within the clock’s 30-day lifetime
Quick start
1. Create a test clock
curl -X POST https://api.elasticpay.co/api/v1/test_helpers/test_clocks \ -H "Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{"name": "Monthly renewal walkthrough", "frozen_time": "2026-01-01T00:00:00Z"}'Response (201 Created):
{ "id": "clk_3kEZpy0UsPbAlvbiGzYEYtSi", "object": "test_clock", "name": "Monthly renewal walkthrough", "status": "created", "frozen_time": "2026-01-01T00:00:00.000Z", "expires_at": "2026-02-08T14:20:05.000Z", "livemode": false}expires_at is 30 days after the clock is created (wall clock), not
30 days after frozen_time.
2. Create a customer attached to the clock
curl -X POST https://api.elasticpay.co/api/v1/customers \ -H "Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "name": "Alex Smith", "email": "alex@example.com", "test_clock": "clk_3kEZpy0UsPbAlvbiGzYEYtSi" }'The test_clock field can only be set at creation and is immutable — you
cannot attach or detach a clock from an existing customer.
3. Advance the clock
Attach a payment plan or subscription to the customer first (see the worked examples below), then advance:
curl -X POST https://api.elasticpay.co/api/v1/test_helpers/test_clocks/clk_3kEZpy0UsPbAlvbiGzYEYtSi/advance \ -H "Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{"frozen_time": "2026-02-01T00:00:00Z"}'Response (202 Accepted) — note the clock still shows the old
frozen_time; the advance completes asynchronously:
{ "id": "clk_3kEZpy0UsPbAlvbiGzYEYtSi", "status": "advancing", "frozen_time": "2026-01-01T00:00:00.000Z"}You’ll receive test_clock.advancing when the advance is accepted, then
test_clock.ready when the billing effects have completed — or poll the
clock until status is ready.
4. Retrieve the updated clock
curl https://api.elasticpay.co/api/v1/test_helpers/test_clocks/clk_3kEZpy0UsPbAlvbiGzYEYtSi \ -H "Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{ "id": "clk_3kEZpy0UsPbAlvbiGzYEYtSi", "status": "ready", "frozen_time": "2026-02-01T00:00:00.000Z", "last_advanced_at": "2026-01-09T14:23:11.000Z"}frozen_time is simulated time; last_advanced_at is the real wall-clock
moment the advance completed.
API reference
The test clock object
| Field | Type | Description |
|---|---|---|
id | string (clk_...) | Unique identifier |
object | "test_clock" | Always "test_clock" |
name | string or null | Optional label for the scenario |
status | enum | created / advancing / ready / failed |
frozen_time | ISO 8601 | Simulated current time for the clock’s customers |
expires_at | ISO 8601 | 30 days after creation; the clock is auto-deleted then |
last_advanced_at | ISO 8601 or null | Wall-clock time of the last completed advance |
livemode | false | Always false — clocks are sandbox-only |
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /api/v1/test_helpers/test_clocks | Create a clock |
GET | /api/v1/test_helpers/test_clocks | List clocks (newest first) |
GET | /api/v1/test_helpers/test_clocks/:id | Retrieve a clock |
POST | /api/v1/test_helpers/test_clocks/:id/advance | Advance a clock |
DELETE | /api/v1/test_helpers/test_clocks/:id | Delete a clock and everything attached to it |
All endpoints require a sandbox secret key with the test_clocks:read
or test_clocks:write scope. A live key returns 403 with code
sandbox_only.
Create — frozen_time (required, ISO 8601), name (optional).
422 if you already have 3 active clocks or frozen_time is missing.
List — limit (default 20, max 100) and starting_after (a clk_...
id) for cursor pagination. Returns { "data": [...], "has_more": true|false }.
Advance — frozen_time (required) must be after the current
frozen_time and within the advance bound. The clock
must be in created or ready status.
Delete — removes the clock and all of its customers, along with their payment plans, payment intents, subscriptions, and mandates. This is irreversible.
The advance bound
A single advance can move time forward by at most two billing intervals of the shortest active plan or subscription attached to the clock’s customers:
max advance target = frozen_time + (2 × shortest_billing_interval)The interval accounts for the plan’s full frequency — a weekly plan is 7
days, a fortnightly plan 14, a monthly plan 30; subscriptions count as 30
days. With no billing objects attached yet, a default 31-day interval
applies, giving a 62-day window. If an advance exceeds the bound, the 422
error states the maximum allowed target time.
Need to go further? Issue multiple advances — each fires its own
test_clock.advancing / test_clock.ready pair, so you observe each cycle’s
effects incrementally.
Webhook events
Delivered to your sandbox webhook subscriptions only:
| Event | Fires when | Notable payload fields |
|---|---|---|
test_clock.created | Clock created | Clock object |
test_clock.advancing | Advance accepted | Clock object + target_time |
test_clock.ready | Advance completed | Clock object + effects_manifest |
test_clock.deleted | Clock deleted (manually or at expiry) | Clock object at deletion |
There is no failure webhook — if an advance fails, the clock’s status
becomes failed (see the FAQ).
The effects_manifest in test_clock.ready summarises what the advance
produced:
{ "payment_intents_enqueued": 2, "links_expired": 1, "subscriptions_rolled": 1, "plans_recalculated": 2}Worked examples
Test a monthly payment plan renewal
Create clock at Jan 1Create customer attached to the clockCreate a monthly payment plan for that customer, start_date Jan 1 → plan activates; first payment scheduled for Jan 1Advance to Jan 1 noon → test_clock.ready: payment_intents_enqueued: 1 — first payment succeedsAdvance to Feb 1 → test_clock.ready: payment_intents_enqueued: 1 — renewal fired → check the payment intent: succeeded; next payment scheduled Mar 1Test dunning — a payment fails, the retry fires
Create clock at Jan 1Create customer + plan where the collection will decline (force the outcome — see Forcing payment outcomes on the Test Cards page)Advance to Jan 1 noon → payment dispatches and fails; the plan's failure handling queues a retryAdvance to Jan 4 (the retry window) → test_clock.ready: payment_intents_enqueued: 1 → check whether the retry succeeded or failedTest payment-link expiry
Create clock at Jan 1, customer attachedCreate a payment link expiring Jan 7 — status: activeAdvance to Jan 8 → test_clock.ready: links_expired: 1 → link status is expired and no longer payableTest subscription period rollover
Create clock at Jan 1, customer with a monthly subscriptionAdvance to Feb 1 → test_clock.ready: subscriptions_rolled: 1 → current period is now Feb 1 – Mar 1Limits and lifetime
| Limit | Value |
|---|---|
| Customers per clock | 3 |
| Active clocks per sandbox account | 3 |
| Clock-bound customers per sandbox account | 9 |
| Clock lifetime | 30 days from creation |
| Advance direction | Forward only — no rewind |
| Advance bound per call | 2 billing intervals |
When a clock expires or is deleted, all of its customers and their billing objects are deleted permanently. Design scenarios around this lifetime, or delete and recreate clocks between test sessions.
Not everything moves with the clock — see What Test Clocks Simulate before building scenarios around settlement or BECS collection outcomes.
FAQ
Can I use a live API key?
No. test_helpers endpoints reject live keys with 403 sandbox_only.
Can I attach an existing customer to a clock?
No — test_clock is set at customer creation and is immutable. Create a new
customer on the clock.
Can I rewind? No. Clocks are forward-only. To re-run a scenario, delete the clock (which deletes its customers) and create a fresh one.
What happens at expiry? After 30 days the clock, its customers, and their billing objects are deleted automatically. There is no recovery.
My advance was rejected for exceeding the bound.
You asked for a target beyond frozen_time + 2 × shortest interval — the
error message includes the maximum allowed time. Advance in smaller steps.
The clock is stuck in advancing or shows failed.
A failed clock cannot be advanced again — delete it and recreate the
scenario. If a clock stays in advancing unusually long, check its status
in the dashboard (Developer → Test Clocks).