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:

SimulatorOperationHappy cycle
Wallet transferPOST/v1/gateway/sandbox/wallet/simulate/PENDING → PROCESSING → SETTLED
Credit disbursementPOST/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”):

AmountWallet transferCredit disbursement
22.22PENDING → PROCESSING → FAILEDPENDING → PROCESSING → FAILED
33.33PENDING → PROCESSING → SETTLED → RETURNEDPENDING → PROCESSING → DISBURSED → RETURNED
11.11 or any otherPENDING → PROCESSING → SETTLEDPENDING → 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 assumes PENDING (and responds PROCESSING).
  • A status that 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 as reference, 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 withDisbursement cyclereason_code
CC 1000000001 (eligible), amount ≤ 50,000,000Depends on the magic amountDISBURSEMENT_FAILED / DISBURSEMENT_RETURNED where applicable
CC 1000000001, amount > 50,000,000PENDING → PROCESSING → FAILEDAMOUNT_EXCEEDS_LIMIT
CC 1000000002 (ineligible)PENDING → PROCESSING → FAILEDNOT_ELIGIBLE
CC 1000000003 (manual review)PENDING → PROCESSING → FAILEDMANUAL_REVIEW
CC 1000000004 (with debts)PENDING → PROCESSING → FAILEDOUTSTANDING_DEBT

The response #

JSON
{
  "object": "wallet_transfer",
  "previous_status": "PROCESSING",
  "status": "FAILED",
  "terminal": true,
  "amount": 22.22,
  "reference": "NOVA-P2P-2026-000123",
  "reason_code": "TRANSFER_FAILED",
  "environment": "sandbox"
}
FieldWhat it is
objectwallet_transfer or disbursement.
previous_status / statusThe state you sent and the next one.
terminaltrue when status is the end of the cycle: stop calling.
amountThe amount you sent (here, unlike the rest of the API, as a number).
referenceYour transaction_id (or the application id, for the disbursement).
reason_codeOnly on FAILED or RETURNED: TRANSFER_FAILED, TRANSFER_RETURNED, DISBURSEMENT_FAILED, DISBURSEMENT_RETURNED or the profile’s reason.
profileDisbursement 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.

Returned transfer (33.33) up to its terminal state
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_RETURNED

A credit disbursement #

First submit an application in the sandbox (with CC 1000000001 for the happy case) and use its application_id in the simulator:

Disbursement of an eligible credit
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 → DISBURSED

Change 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 FAILED or a RETURNED arrives.
  • 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 #

SDKTransferDisbursement
Pythonclient.sandbox.simulate.wallet_transfer(status=, amount=, transaction_id=, extra=)client.sandbox.simulate.credit_disbursement(app_id, status=, amount=, extra=)
JavaScript / TSclient.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:)
.NETclient.Sandbox.Simulate.WalletTransferAsync(status, amount, transactionId, extra)client.Sandbox.Simulate.CreditDisbursementAsync(appId, status, amount, extra)
Goclient.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).