---
title: Test Clocks
description: Simulate the passage of time to test billing flows in minutes, not months.
editUrl: true
head: []
template: doc
sidebar:
  hidden: false
  attrs: {}
pagefind: true
draft: false
---

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

1. **Create a clock** with a starting `frozen_time` (the simulated "now")
2. **Create customers attached to the clock** — everywhere the billing engine
   asks "what time is it?", these customers get the clock's time
3. **Attach billing objects** to those customers: payment plans,
   subscriptions, payment links
4. **Advance the clock** to a new `frozen_time` — the advance runs
   asynchronously
5. **Observe the effects** — the `test_clock.ready` webhook reports what
   fired, and the affected objects are in their post-advance states
6. **Repeat** as needed within the clock's 30-day lifetime

## Quick start

### 1. Create a test clock

```bash
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`):

```json
{
  "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

```bash
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.

:::note
Clock-bound customers are **excluded from the default customer list**. To see
them, pass the clock explicitly:
`GET /api/v1/customers?test_clock=clk_3kEZpy0UsPbAlvbiGzYEYtSi`.
:::

### 3. Advance the clock

Attach a payment plan or subscription to the customer first (see the
[worked examples](#worked-examples) below), then advance:

```bash
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:

```json
{
  "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

```bash
curl https://api.elasticpay.co/api/v1/test_helpers/test_clocks/clk_3kEZpy0UsPbAlvbiGzYEYtSi \
  -H "Authorization: Bearer sk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

```json
{
  "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-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](#faq)).

The `effects_manifest` in `test_clock.ready` summarises what the advance
produced:

```json
{
  "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 1
Create customer attached to the clock
Create a monthly payment plan for that customer, start_date Jan 1
  → plan activates; first payment scheduled for Jan 1
Advance to Jan 1 noon
  → test_clock.ready: payment_intents_enqueued: 1 — first payment succeeds
Advance to Feb 1
  → test_clock.ready: payment_intents_enqueued: 1 — renewal fired
  → check the payment intent: succeeded; next payment scheduled Mar 1
```

### Test dunning — a payment fails, the retry fires

```
Create clock at Jan 1
Create 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 retry
Advance to Jan 4 (the retry window)
  → test_clock.ready: payment_intents_enqueued: 1
  → check whether the retry succeeded or failed
```

### Test payment-link expiry

```
Create clock at Jan 1, customer attached
Create a payment link expiring Jan 7 — status: active
Advance to Jan 8
  → test_clock.ready: links_expired: 1
  → link status is expired and no longer payable
```

### Test subscription period rollover

```
Create clock at Jan 1, customer with a monthly subscription
Advance to Feb 1
  → test_clock.ready: subscriptions_rolled: 1
  → current period is now Feb 1 – Mar 1
```

## Limits 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](/api/concepts/test-clock-simulation) 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**).