Sandbox
Lifecycle simulators
Advance a wallet transfer (PENDING → PROCESSING → SETTLED) or a credit disbursement (PENDING → PROCESSING → DISBURSED) one state at a time, and reach FAILED or RETURNED with the magic amounts 22.22 and 33.33.
On this page
Your integration has to react to every state a money movement goes through: show “in progress”, confirm when it settles, reverse if it fails, reconcile if it’s returned. The simulators let you walk through those states in the sandbox one step at a time and reach every terminal state (failures included) without waiting on any provider.
There are two simulators:
| Simulator | Operation | Happy cycle |
|---|---|---|
| Wallet transfer | POST/v1/gateway/sandbox/wallet/simulate/ | PENDING → PROCESSING → SETTLED |
| Credit disbursement | POST/v1/gateway/sandbox/credits/{app_id}/advance/ | PENDING → PROCESSING → DISBURSED |
Sandbox keys only
A production key gets 403 on these routes, regardless of the URL. They aren’t writes: they don’t take
an Idempotency-Key, they don’t return the transaction envelope and they never move money.
How they work #
They’re pure transformations: you pass them the current state (the one you have stored) and
they return the next one. Nothing is stored on the gateway side, so the same input always produces
the same output. To advance, call again with the status returned by the previous call.
The amount decides the terminal state (the “magic amounts”):
| Amount | Wallet transfer | Credit disbursement |
|---|---|---|
22.22 | PENDING → PROCESSING → FAILED | PENDING → PROCESSING → FAILED |
33.33 | PENDING → PROCESSING → SETTLED → RETURNED | PENDING → PROCESSING → DISBURSED → RETURNED |
11.11 or any other | PENDING → PROCESSING → SETTLED | PENDING → PROCESSING → DISBURSED |
RETURNED models a movement that settled and was later returned: it goes through the happy state
first. The amount is compared to the cent (33.33 and 33.330 are the same).
stateDiagram-v2
[*] --> PENDING
PENDING --> PROCESSING
PROCESSING --> SETTLED: any amount except 22.22
PROCESSING --> FAILED: amount 22.22
SETTLED --> RETURNED: amount 33.33
SETTLED --> [*]
FAILED --> [*]
RETURNED --> [*](For the disbursement, replace SETTLED with DISBURSED.)
Other rules:
- Without
status, the simulator assumesPENDING(and respondsPROCESSING). - A
statusthat doesn’t belong to the cycle is treated as the start. - In a terminal state, the simulator stays there: calling it again returns the same state.
- If you send
transaction_id, it comes back asreference, which is useful for correlating in your logs.
The disbursement respects the application’s profile #
For the disbursement, the route’s app_id is the id of a sandbox credit application (sbx-…, the one
you got back when you submitted it). If that application can’t be approved according to its
test profile (ineligible, with debts, in
manual review or above the amount threshold), the disbursement can’t happen and the simulator goes
straight to FAILED, with the profile’s reason in reason_code:
| Application submitted with | Disbursement cycle | reason_code |
|---|---|---|
CC 1000000001 (eligible), amount ≤ 50,000,000 | Depends on the magic amount | DISBURSEMENT_FAILED / DISBURSEMENT_RETURNED where applicable |
CC 1000000001, amount > 50,000,000 | PENDING → PROCESSING → FAILED | AMOUNT_EXCEEDS_LIMIT |
CC 1000000002 (ineligible) | PENDING → PROCESSING → FAILED | NOT_ELIGIBLE |
CC 1000000003 (manual review) | PENDING → PROCESSING → FAILED | MANUAL_REVIEW |
CC 1000000004 (with debts) | PENDING → PROCESSING → FAILED | OUTSTANDING_DEBT |
The response #
{
"object": "wallet_transfer",
"previous_status": "PROCESSING",
"status": "FAILED",
"terminal": true,
"amount": 22.22,
"reference": "NOVA-P2P-2026-000123",
"reason_code": "TRANSFER_FAILED",
"environment": "sandbox"
}| Field | What it is |
|---|---|
object | wallet_transfer or disbursement. |
previous_status / status | The state you sent and the next one. |
terminal | true when status is the end of the cycle: stop calling. |
amount | The amount you sent (here, unlike the rest of the API, as a number). |
reference | Your transaction_id (or the application id, for the disbursement). |
reason_code | Only on FAILED or RETURNED: TRANSFER_FAILED, TRANSFER_RETURNED, DISBURSEMENT_FAILED, DISBURSEMENT_RETURNED or the profile’s reason. |
profile | Disbursement only: the application’s test profile. |
Advancing to the terminal state #
The pattern is a loop: call with the current state until terminal is true. At each step, run what
your system would do on receiving that state (update your record, notify the affiliate, reverse), so
you test every branch.
import os
from compense_gateway import CompenseClient
client = CompenseClient(api_key=os.environ["COMPENSE_API_KEY"], environment="sandbox")
estado = "PENDING"
while True:
paso = client.sandbox.simulate.wallet_transfer(
status=estado, amount="33.33", transaction_id="NOVA-P2P-2026-000123")
print(paso["previous_status"], "→", paso["status"], paso.get("reason_code", ""))
aplicar_estado(paso) # your logic for each state
if paso["terminal"]:
break
estado = paso["status"]
# PENDING → PROCESSING · PROCESSING → SETTLED · SETTLED → RETURNED TRANSFER_RETURNEDimport { CompenseClient } from "@compense/sdk";
const client = new CompenseClient({ apiKey: process.env.COMPENSE_API_KEY!, environment: "sandbox" });
let estado = "PENDING";
for (;;) {
const paso = await client.sandbox.simulate.walletTransfer({
status: estado, amount: "33.33", transactionId: "NOVA-P2P-2026-000123",
});
console.log(paso.previous_status, "→", paso.status, paso.reason_code ?? "");
aplicarEstado(paso); // your logic for each state
if (paso.terminal === true) break;
estado = String(paso.status);
}<?php
require __DIR__ . '/vendor/autoload.php';
use Compense\Sdk\CompenseClient;
$client = new CompenseClient(apiKey: getenv('COMPENSE_API_KEY'), environment: 'sandbox');
$estado = 'PENDING';
while (true) {
$paso = $client->sandbox->simulate->walletTransfer(
status: $estado, amount: '33.33', transactionId: 'NOVA-P2P-2026-000123');
echo $paso['previous_status'], ' → ', $paso['status'], ' ', $paso['reason_code'] ?? '', PHP_EOL;
aplicarEstado($paso); // your logic for each state
if ($paso['terminal'] === true) {
break;
}
$estado = $paso['status'];
}using System.Text.Json.Nodes;
using Compense.Sdk;
using var client = new CompenseClient(
Environment.GetEnvironmentVariable("COMPENSE_API_KEY")!, CompenseEnvironment.Sandbox);
string estado = "PENDING";
while (true)
{
JsonObject paso = await client.Sandbox.Simulate.WalletTransferAsync(
status: estado, amount: "33.33", transactionId: "NOVA-P2P-2026-000123");
Console.WriteLine($"{paso["previous_status"]} → {paso["status"]} {paso["reason_code"]}");
AplicarEstado(paso); // your logic for each state
if (paso["terminal"]?.GetValue<bool>() == true) break;
estado = paso["status"]!.GetValue<string>();
}package main
import (
"context"
"fmt"
"log"
"os"
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)
}
estado := &compense.SimulationState{Status: "PENDING", Amount: "33.33", TransactionID: "NOVA-P2P-2026-000123"}
for {
paso, err := client.Sandbox.Simulate.WalletTransfer(ctx, estado)
if err != nil {
log.Fatal(err)
}
fmt.Println(paso["previous_status"], "→", paso["status"], paso["reason_code"])
aplicarEstado(paso) // your logic for each state
if terminal, _ := paso["terminal"].(bool); terminal {
break
}
estado.Status = fmt.Sprint(paso["status"])
}
}URL="https://sandbox.dev.compense.co/gw/v1/gateway/sandbox/wallet/simulate/"
ESTADO="PENDING"
while :; do
PASO=$(curl -s -X POST "$URL" \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"status\": \"$ESTADO\", \"amount\": \"33.33\", \"transaction_id\": \"NOVA-P2P-2026-000123\"}")
echo "$PASO" | jq -r '"\(.previous_status) → \(.status) \(.reason_code // "")"'
[ "$(echo "$PASO" | jq -r '.terminal')" = "true" ] && break
ESTADO=$(echo "$PASO" | jq -r '.status')
doneA credit disbursement #
First submit an application in the sandbox (with CC 1000000001 for the happy case) and use its
application_id in the simulator:
tx = client.credits.submit_application(
affiliate={"document_type": "CC", "document_number": "1000000001"},
product_code="LIBRANZA_12M", amount="3500000", term_months=12,
external_reference="HORIZONTE-CRED-2026-000123",
)
app_id = tx.data["application_id"] # sbx-E-…
estado = "PENDING"
while True:
paso = client.sandbox.simulate.credit_disbursement(app_id, status=estado)
print(paso["previous_status"], "→", paso["status"], paso.get("reason_code", ""))
if paso["terminal"]:
break
estado = paso["status"]
# PENDING → PROCESSING · PROCESSING → DISBURSEDconst tx = await client.credits.submitApplication({
affiliate: { document_type: "CC", document_number: "1000000001" },
productCode: "LIBRANZA_12M", amount: "3500000", termMonths: 12,
externalReference: "HORIZONTE-CRED-2026-000123",
});
const appId = String(tx.data.application_id); // sbx-E-…
let estado = "PENDING";
for (;;) {
const paso = await client.sandbox.simulate.creditDisbursement(appId, { status: estado });
console.log(paso.previous_status, "→", paso.status, paso.reason_code ?? "");
if (paso.terminal === true) break;
estado = String(paso.status);
}use Compense\Sdk\Model\AffiliateRef;
$tx = $client->credits->submitApplication(
affiliate: new AffiliateRef('CC', '1000000001'),
productCode: 'LIBRANZA_12M', amount: '3500000', termMonths: 12,
externalReference: 'HORIZONTE-CRED-2026-000123',
);
$appId = $tx->data['application_id']; // sbx-E-…
$estado = 'PENDING';
while (true) {
$paso = $client->sandbox->simulate->creditDisbursement($appId, status: $estado);
echo $paso['previous_status'], ' → ', $paso['status'], ' ', $paso['reason_code'] ?? '', PHP_EOL;
if ($paso['terminal'] === true) {
break;
}
$estado = $paso['status'];
}using Compense.Sdk.Models;
Transaction tx = await client.Credits.SubmitApplicationAsync(
affiliate: new AffiliateRef("CC", "1000000001"),
productCode: "LIBRANZA_12M", amount: "3500000", termMonths: 12,
externalReference: "HORIZONTE-CRED-2026-000123");
string appId = tx.Data["application_id"]!.GetValue<string>(); // sbx-E-…
string estado = "PENDING";
while (true)
{
JsonObject paso = await client.Sandbox.Simulate.CreditDisbursementAsync(appId, status: estado);
Console.WriteLine($"{paso["previous_status"]} → {paso["status"]} {paso["reason_code"]}");
if (paso["terminal"]?.GetValue<bool>() == true) break;
estado = paso["status"]!.GetValue<string>();
}tx, err := client.Credits.SubmitApplication(ctx, &compense.CreditApplicationParams{
Affiliate: compense.AffiliateRef{DocumentType: "CC", DocumentNumber: "1000000001"},
ProductCode: "LIBRANZA_12M",
Amount: "3500000",
TermMonths: 12,
ExternalReference: "HORIZONTE-CRED-2026-000123",
})
if err != nil {
log.Fatal(err)
}
appID := fmt.Sprint(tx.Data["application_id"]) // sbx-E-…
estado := &compense.SimulationState{Status: "PENDING"}
for {
paso, err := client.Sandbox.Simulate.CreditDisbursement(ctx, appID, estado)
if err != nil {
log.Fatal(err)
}
fmt.Println(paso["previous_status"], "→", paso["status"], paso["reason_code"])
if terminal, _ := paso["terminal"].(bool); terminal {
break
}
estado.Status = fmt.Sprint(paso["status"])
}# $APP_ID = data.application_id of the submitted application (sbx-E-…)
URL="https://sandbox.dev.compense.co/gw/v1/gateway/sandbox/credits/$APP_ID/advance/"
ESTADO="PENDING"
while :; do
PASO=$(curl -s -X POST "$URL" \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"status\": \"$ESTADO\"}")
echo "$PASO" | jq -r '"\(.previous_status) → \(.status) \(.reason_code // "")"'
[ "$(echo "$PASO" | jq -r '.terminal')" = "true" ] && break
ESTADO=$(echo "$PASO" | jq -r '.status')
doneChange the document to 1000000002 and you’ll see PENDING → PROCESSING → FAILED with
reason_code: "NOT_ELIGIBLE"; send amount: "22.22" to the simulator to force a provider failure on
an eligible credit.
What they test and what they don’t #
- Yes: your state machine (every transition, every terminal state, failures and returns), your
messages to the affiliate and your reconciliation when a
FAILEDor aRETURNEDarrives. - No: the actual balance movement. The simulators don’t touch the sandbox ledger; the sandbox’s wallet operations (P2P, Bre-B, bills) return their final result directly.
- They don’t send webhooks. To test your receiver with these states, build the payload yourself and sign it as in Test your receiver offline.
SDK notes #
| SDK | Transfer | Disbursement |
|---|---|---|
| Python | client.sandbox.simulate.wallet_transfer(status=, amount=, transaction_id=, extra=) | client.sandbox.simulate.credit_disbursement(app_id, status=, amount=, extra=) |
| JavaScript / TS | client.sandbox.simulate.walletTransfer({ status, amount, transactionId, extra }) | client.sandbox.simulate.creditDisbursement(appId, { status, amount, extra }) |
| PHP | $client->sandbox->simulate->walletTransfer(status:, amount:, transactionId:, extra:) | $client->sandbox->simulate->creditDisbursement($appId, status:, amount:, extra:) |
| .NET | client.Sandbox.Simulate.WalletTransferAsync(status, amount, transactionId, extra) | client.Sandbox.Simulate.CreditDisbursementAsync(appId, status, amount, extra) |
| Go | client.Sandbox.Simulate.WalletTransfer(ctx, &compense.SimulationState{...}) | client.Sandbox.Simulate.CreditDisbursement(ctx, appID, &compense.SimulationState{...}) |
They all return the body as is (a dictionary, JSON object, array or map[string]any). extra adds
fields to the state you send. In Python, simulate.wallet_transfer_advance and
simulate.credit_disburse_advance are the deprecated 1.0 names (they delegate to the new ones and
emit a DeprecationWarning). From the console: compense simulate transfer|credit
(see The compense CLI).