Sandbox
Test clocks
Virtual time for the sandbox: create a clock frozen on a date, move it forward days or weeks and use its time as the ’now’ of your delinquency, due-date and permanence tests. Clocks don’t store state: every advance returns a clock with a new id.
On this page
Some behaviors only show up as time passes: an installment that goes past due, an obligation that
comes due, a subsidy that requires a minimum membership period. Testing them by waiting real days isn’t
an option. A test clock is a virtual time source: you freeze it on a date (frozen_time) and
move it forward as many times as you like; its current_time is the “now” of your scenario.
Sandbox keys only
The clock operations (
POST/v1/gateway/sandbox/test-clocks/
,
GET/v1/gateway/sandbox/test-clocks/{clock_id}/
and
POST/v1/gateway/sandbox/test-clocks/{clock_id}/advance/
) return 403 to a production key, regardless of the URL.
How they work #
Clocks don’t store state on the server. All the state (the frozen date and the seconds advanced)
travels inside the id, in the form tcl-<data>-<signature>:
- Every
advancereturns a clock with a NEW id. Always keep the id returned by the latest call and continue from it. The old id is still valid, but it represents the clock before the advance. - The id carries an integrity signature: if you edit it by hand (for example, to “rewind” time)
the gateway rejects it with
422. - Since the result depends only on the id and the advance, the same
advanceon the same id always returns the same clock. That’s why these routes don’t useIdempotency-Key. - They only move forward:
delta_secondsmust be>= 0and at most 63 days (5,443,200 seconds) per call. For bigger jumps, advance several times. frozen_timeaccepts ISO-8601 (2026-10-01T00:00:00Z) or Unix seconds. A date without a time zone is interpreted as UTC.
sequenceDiagram
participant T as Your test
participant G as Gateway (sandbox)
T->>G: create(frozen_time = 2026-10-01T00:00:00Z)
G-->>T: tcl-A (current_time 2026-10-01, advanced 0)
T->>G: advance(tcl-A, 30 days)
G-->>T: tcl-B (current_time 2026-10-31, advanced 2.592.000)
T->>G: advance(tcl-B, 15 days)
G-->>T: tcl-C (current_time 2026-11-15, advanced 3.888.000)
T->>G: get(tcl-B)
G-->>T: tcl-B is still at 2026-10-31Create, advance and retrieve #
import os
from compense_gateway import CompenseClient
client = CompenseClient(api_key=os.environ["COMPENSE_API_KEY"], environment="sandbox")
reloj = client.sandbox.test_clocks.create(frozen_time="2026-10-01T00:00:00Z")
print(reloj.id, reloj.current_time) # tcl-… 2026-10-01T00:00:00+00:00
reloj = client.sandbox.test_clocks.advance(reloj.id, delta_seconds=30 * 86400)
print(reloj.id, reloj.current_time, reloj.advanced_seconds) # NEW id, +30 days
mismo = client.sandbox.test_clocks.get(reloj.id)
print(mismo.frozen_time, mismo.current_time, mismo.status) # readyimport { CompenseClient } from "@compense/sdk";
const client = new CompenseClient({ apiKey: process.env.COMPENSE_API_KEY!, environment: "sandbox" });
let reloj = await client.sandbox.testClocks.create({ frozenTime: "2026-10-01T00:00:00Z" });
console.log(reloj.id, reloj.currentTime);
reloj = await client.sandbox.testClocks.advance(reloj.id, { deltaSeconds: 30 * 86400 });
console.log(reloj.id, reloj.currentTime, reloj.advancedSeconds); // NEW id, +30 days
const mismo = await client.sandbox.testClocks.get(reloj.id);
console.log(mismo.frozenTime, mismo.currentTime, mismo.status);<?php
require __DIR__ . '/vendor/autoload.php';
use Compense\Sdk\CompenseClient;
$client = new CompenseClient(apiKey: getenv('COMPENSE_API_KEY'), environment: 'sandbox');
$reloj = $client->sandbox->testClocks->create('2026-10-01T00:00:00Z'); // or a \DateTimeInterface
echo $reloj->id, ' ', $reloj->currentTime, PHP_EOL;
$reloj = $client->sandbox->testClocks->advance($reloj->id, deltaSeconds: 30 * 86400);
echo $reloj->id, ' ', $reloj->currentTime, ' ', $reloj->advancedSeconds, PHP_EOL; // NEW id
$mismo = $client->sandbox->testClocks->get($reloj->id);
echo $mismo->frozenTime, ' ', $mismo->currentTime, ' ', $mismo->status, PHP_EOL;using Compense.Sdk;
using Compense.Sdk.Models;
using var client = new CompenseClient(
Environment.GetEnvironmentVariable("COMPENSE_API_KEY")!, CompenseEnvironment.Sandbox);
TestClock reloj = await client.Sandbox.TestClocks.CreateAsync("2026-10-01T00:00:00Z"); // or DateTimeOffset / Unix seconds
Console.WriteLine($"{reloj.Id} {reloj.CurrentTime}");
reloj = await client.Sandbox.TestClocks.AdvanceAsync(reloj.Id, 30 * 86400);
Console.WriteLine($"{reloj.Id} {reloj.CurrentTime} {reloj.AdvancedSeconds}"); // NEW id
TestClock mismo = await client.Sandbox.TestClocks.GetAsync(reloj.Id);
Console.WriteLine($"{mismo.FrozenTime} {mismo.CurrentTime} {mismo.Status}");package main
import (
"context"
"fmt"
"log"
"os"
"time"
compense "github.com/Appiasoft/compense-saas-sdk-go"
)
func main() {
ctx := context.Background()
client, err := compense.NewClient(os.Getenv("COMPENSE_API_KEY"),
compense.WithEnvironment(compense.Sandbox))
if err != nil {
log.Fatal(err)
}
reloj, err := client.Sandbox.TestClocks.Create(ctx, &compense.CreateTestClockParams{
FrozenTime: time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC),
})
if err != nil {
log.Fatal(err)
}
fmt.Println(reloj.ID, reloj.CurrentTime)
reloj, err = client.Sandbox.TestClocks.Advance(ctx, reloj.ID,
&compense.AdvanceTestClockParams{DeltaSeconds: 30 * 86400})
if err != nil {
log.Fatal(err)
}
fmt.Println(reloj.ID, reloj.CurrentTime, reloj.AdvancedSeconds) // NEW id
mismo, err := client.Sandbox.TestClocks.Get(ctx, reloj.ID)
if err != nil {
log.Fatal(err)
}
fmt.Println(mismo.FrozenTime, mismo.CurrentTime, mismo.Status)
}BASE="https://sandbox.dev.compense.co/gw/v1/gateway/sandbox/test-clocks"
curl -X POST "$BASE/" \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"frozen_time": "2026-10-01T00:00:00Z"}'
curl -X POST "$BASE/$CLOCK_ID/advance/" \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"delta_seconds": 2592000}'
curl "$BASE/$CLOCK_ID/" -H "X-API-Key: $COMPENSE_API_KEY"The clock looks like this (in cURL, create and advance return it inside the write envelope’s
data, and get returns it directly; the SDKs unwrap the envelope for you):
{
"id": "tcl-MTc5MDgxMjgwMC4yNTkyMDAw-9fd1bf0d88220638",
"object": "test_clock",
"status": "ready",
"frozen_time": "2026-10-01T00:00:00+00:00",
"current_time": "2026-10-31T00:00:00+00:00",
"advanced_seconds": 2592000,
"drives": ["loan_aging_dpd", "provisioning_rollforward", "subsidy_permanence"],
"environment": "sandbox"
}| Field | What it is |
|---|---|
id | The clock. It changes on every advance. |
frozen_time | The date you froze it on when you created it. It doesn’t change. |
advanced_seconds | Total seconds advanced since frozen_time. |
current_time | frozen_time + advanced_seconds: the “now” of your scenario. |
drives | What it’s designed for: delinquency aging (loan_aging_dpd), provisions (provisioning_rollforward) and subsidy permanence (subsidy_permanence). |
The clock is your source of 'now', not the gateway's
In this version no other sandbox operation receives a clock id: the sandbox’s loan portfolio, health
and PQRS dates are fixed (for example, the first installment of the test loans is due on July 15,
2026). Use current_time as the “now” of your system during the test (inject it into your date
service) and compare it with the dates the sandbox returns.
Use case: loan delinquency #
You want to test that your system correctly classifies an installment’s delinquency by days past due
(0, 30, 60, 90 days or more). The first installment of the test loans is due on July 15, 2026.
Freeze a clock on that day and move it forward in stages; since each call allows at most 63 days, the
example advances in jumps and evaluates your classification function at each current_time.
from datetime import datetime
MAX = 63 * 86400
reloj = client.sandbox.test_clocks.create(frozen_time="2026-07-15T00:00:00Z")
vence = datetime.fromisoformat(reloj.frozen_time)
pendiente = 95 * 86400
while pendiente > 0:
paso = min(pendiente, MAX)
reloj = client.sandbox.test_clocks.advance(reloj.id, delta_seconds=paso)
pendiente -= paso
dias = (datetime.fromisoformat(reloj.current_time) - vence).days
print(reloj.current_time, dias, clasificar_mora(dias)) # your functionconst MAX = 63 * 86400;
let reloj = await client.sandbox.testClocks.create({ frozenTime: "2026-07-15T00:00:00Z" });
const vence = Date.parse(reloj.frozenTime);
let pendiente = 95 * 86400;
while (pendiente > 0) {
const paso = Math.min(pendiente, MAX);
reloj = await client.sandbox.testClocks.advance(reloj.id, { deltaSeconds: paso });
pendiente -= paso;
const dias = Math.floor((Date.parse(reloj.currentTime) - vence) / 86_400_000);
console.log(reloj.currentTime, dias, clasificarMora(dias)); // your function
}$max = 63 * 86400;
$reloj = $client->sandbox->testClocks->create('2026-07-15T00:00:00Z');
$vence = new DateTimeImmutable($reloj->frozenTime);
$pendiente = 95 * 86400;
while ($pendiente > 0) {
$paso = min($pendiente, $max);
$reloj = $client->sandbox->testClocks->advance($reloj->id, deltaSeconds: $paso);
$pendiente -= $paso;
$dias = $vence->diff(new DateTimeImmutable($reloj->currentTime))->days;
echo $reloj->currentTime, ' ', $dias, ' ', clasificarMora($dias), PHP_EOL; // your function
}const long Max = 63 * 86400;
TestClock reloj = await client.Sandbox.TestClocks.CreateAsync("2026-07-15T00:00:00Z");
DateTimeOffset vence = DateTimeOffset.Parse(reloj.FrozenTime);
long pendiente = 95 * 86400;
while (pendiente > 0)
{
long paso = Math.Min(pendiente, Max);
reloj = await client.Sandbox.TestClocks.AdvanceAsync(reloj.Id, paso);
pendiente -= paso;
int dias = (int)(DateTimeOffset.Parse(reloj.CurrentTime) - vence).TotalDays;
Console.WriteLine($"{reloj.CurrentTime} {dias} {ClasificarMora(dias)}"); // your function
}const max = int64(63 * 86400)
reloj, err := client.Sandbox.TestClocks.Create(ctx, &compense.CreateTestClockParams{
FrozenTime: time.Date(2026, 7, 15, 0, 0, 0, 0, time.UTC),
})
if err != nil {
log.Fatal(err)
}
vence, _ := time.Parse(time.RFC3339, reloj.FrozenTime)
for pendiente := int64(95 * 86400); pendiente > 0; {
paso := min(pendiente, max)
reloj, err = client.Sandbox.TestClocks.Advance(ctx, reloj.ID, &compense.AdvanceTestClockParams{DeltaSeconds: paso})
if err != nil {
log.Fatal(err)
}
pendiente -= paso
ahora, _ := time.Parse(time.RFC3339, reloj.CurrentTime)
dias := int(ahora.Sub(vence).Hours() / 24)
fmt.Println(reloj.CurrentTime, dias, clasificarMora(dias)) // your function
}BASE="https://sandbox.dev.compense.co/gw/v1/gateway/sandbox/test-clocks"
ID=$(curl -s -X POST "$BASE/" -H "X-API-Key: $COMPENSE_API_KEY" -H "Content-Type: application/json" \
-d '{"frozen_time": "2026-07-15T00:00:00Z"}' | jq -r '.data.id // .id')
for PASO in 5443200 2764800; do # 63 days + 32 days = 95 days
ID=$(curl -s -X POST "$BASE/$ID/advance/" -H "X-API-Key: $COMPENSE_API_KEY" \
-H "Content-Type: application/json" -d "{\"delta_seconds\": $PASO}" | jq -r '.data.id // .id')
curl -s "$BASE/$ID/" -H "X-API-Key: $COMPENSE_API_KEY" | jq -r '.current_time'
doneCombine it with the test loan that’s 95 days past due (CC 1000000004, status DEFAULTED) to
compare your calculation with what the sandbox loan portfolio
returns.
Other typical scenarios:
- Due dates: freeze the clock the day before an obligation is due, advance 1 day and check that your system generates the notice or the late fee.
- Reminders: advance in 24-hour jumps and check that your scheduled job sends each reminder only once.
- Permanence: advance months (in jumps of up to 63 days) to test rules that require a minimum membership period.
Errors #
| HTTP | When |
|---|---|
403 | The key is a production key. |
422 | frozen_time is empty or is neither ISO-8601 nor Unix seconds; the id isn’t a clock (tcl-…) or was tampered with; delta_seconds is negative, not an integer or greater than 63 days. |
SDK notes #
- Python:
advance(..., by_seconds=...)is the deprecated (1.0) name fordelta_seconds;advance(..., to=...)doesn’t exist in the gateway and raisesCompenseConfigError;create(name=...)is ignored with a warning (clocks don’t have names). The model isTestClock(id,frozen_time,current_time,advanced_seconds,status,raw). - JavaScript / TS:
create({ frozenTime })accepts ISO-8601 text or Unix seconds. - PHP:
create()accepts text, Unix seconds (int) or\DateTimeInterface. - .NET:
CreateAsynchas overloads forstring,long(Unix seconds) andDateTimeOffset;AdvanceAsync(id, deltaSeconds)takes along. - Go:
CreateTestClockParams.FrozenTimeis atime.Time(sent as RFC 3339 in UTC);AdvanceTestClockParams.DeltaSecondsis anint64. - CLI:
compense clock create|get|advance(see The compense CLI).