Tu integración tiene que reaccionar a cada estado por el que pasa un movimiento de dinero: mostrar “en proceso”, confirmar al liquidarse, revertir si falla, conciliar si lo devuelven. Los simuladores te permiten recorrer esos estados en el sandbox un paso a la vez y llegar a cada estado terminal (incluidos los fallos) sin esperar a ningún proveedor.

Hay dos simuladores:

SimuladorOperaciónCiclo feliz
Transferencia de billeteraPOST/v1/gateway/sandbox/wallet/simulate/PENDING → PROCESSING → SETTLED
Desembolso de créditoPOST/v1/gateway/sandbox/credits/{app_id}/advance/PENDING → PROCESSING → DISBURSED

Solo con llaves de sandbox

Una llave de producción recibe 403 en estas rutas, sin importar la URL. No son escrituras: no llevan Idempotency-Key, no devuelven el sobre de transacción y nunca mueven dinero.

Cómo funcionan #

Son transformaciones puras: les pasas el estado actual (el que tienes guardado) y te devuelven el siguiente. No hay nada guardado del lado del gateway, así que la misma entrada da siempre la misma salida. Para avanzar, vuelve a llamar con el status que te devolvió la llamada anterior.

El estado terminal lo decide el monto (los “montos mágicos”):

MontoTransferencia de billeteraDesembolso de crédito
22.22PENDING → PROCESSING → FAILEDPENDING → PROCESSING → FAILED
33.33PENDING → PROCESSING → SETTLED → RETURNEDPENDING → PROCESSING → DISBURSED → RETURNED
11.11 o cualquier otroPENDING → PROCESSING → SETTLEDPENDING → PROCESSING → DISBURSED

RETURNED modela un movimiento que se liquidó y luego fue devuelto: pasa primero por el estado feliz. El monto se compara al centavo (33.33 y 33.330 son lo mismo).

stateDiagram-v2
    [*] --> PENDING
    PENDING --> PROCESSING
    PROCESSING --> SETTLED: cualquier monto salvo 22.22
    PROCESSING --> FAILED: monto 22.22
    SETTLED --> RETURNED: monto 33.33
    SETTLED --> [*]
    FAILED --> [*]
    RETURNED --> [*]

(Para el desembolso, cambia SETTLED por DISBURSED.)

Otras reglas:

  • Sin status, el simulador asume PENDING (y responde PROCESSING).
  • Un status que no pertenece al ciclo se trata como el inicio.
  • En un estado terminal, el simulador se queda ahí: llamarlo otra vez devuelve el mismo estado.
  • Si envías transaction_id, vuelve como reference, útil para correlacionar en tus registros.

El desembolso respeta el perfil de la solicitud #

Para el desembolso, el app_id de la ruta es el id de una solicitud de crédito del sandbox (sbx-…, el que te devolvió radicarla). Si esa solicitud no es aprobable según su perfil de prueba (inelegible, con deudas, en revisión manual o por encima del umbral de monto), el desembolso no puede ocurrir y el simulador va directo a FAILED, con el motivo del perfil en reason_code:

Solicitud radicada conCiclo del desembolsoreason_code
CC 1000000001 (elegible), monto ≤ 50.000.000Según el monto mágicoDISBURSEMENT_FAILED / DISBURSEMENT_RETURNED si aplica
CC 1000000001, monto > 50.000.000PENDING → PROCESSING → FAILEDAMOUNT_EXCEEDS_LIMIT
CC 1000000002 (inelegible)PENDING → PROCESSING → FAILEDNOT_ELIGIBLE
CC 1000000003 (revisión manual)PENDING → PROCESSING → FAILEDMANUAL_REVIEW
CC 1000000004 (con deudas)PENDING → PROCESSING → FAILEDOUTSTANDING_DEBT

La respuesta #

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"
}
CampoQué es
objectwallet_transfer o disbursement.
previous_status / statusEl estado que enviaste y el siguiente.
terminaltrue cuando status es el final del ciclo: deja de llamar.
amountEl monto que enviaste (aquí, a diferencia del resto del API, como número).
referenceTu transaction_id (o el id de la solicitud, en el desembolso).
reason_codeSolo en FAILED o RETURNED: TRANSFER_FAILED, TRANSFER_RETURNED, DISBURSEMENT_FAILED, DISBURSEMENT_RETURNED o el motivo del perfil.
profileSolo en el desembolso: el perfil de prueba de la solicitud.

Avanzar hasta el estado terminal #

El patrón es un bucle: llama con el estado actual hasta que terminal sea true. En cada paso, ejecuta lo que tu sistema haría al recibir ese estado (actualizar tu registro, notificar al afiliado, revertir), para probar cada rama.

Transferencia devuelta (33.33) hasta su estado terminal
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)                         # tu lógica para cada estado
    if paso["terminal"]:
        break
    estado = paso["status"]
# PENDING → PROCESSING · PROCESSING → SETTLED · SETTLED → RETURNED TRANSFER_RETURNED

El desembolso de un crédito #

Radica primero una solicitud en el sandbox (con CC 1000000001 para el caso feliz) y usa su application_id en el simulador:

Desembolso de un crédito elegible
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

Cambia el documento a 1000000002 y verás PENDING → PROCESSING → FAILED con reason_code: "NOT_ELIGIBLE"; envía amount: "22.22" en el simulador para forzar el fallo del proveedor sobre un crédito elegible.

Qué prueban y qué no #

  • Sí: tu máquina de estados (cada transición, cada estado terminal, los fallos y las devoluciones), tus mensajes al afiliado y tu conciliación ante un FAILED o un RETURNED.
  • No: el movimiento real de saldo. Los simuladores no tocan el libro mayor del sandbox; las operaciones de billetera del sandbox (P2P, Bre-B, facturas) responden directamente su resultado final.
  • No emiten webhooks. Para probar tu receptor con estos estados, arma el payload tú mismo y fírmalo como en Prueba tu receptor sin red.

Notas por SDK #

SDKTransferenciaDesembolso
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{...})

Todos devuelven el cuerpo tal cual (un diccionario, objeto JSON, arreglo o map[string]any). extra agrega campos al estado que envías. En Python, simulate.wallet_transfer_advance y simulate.credit_disburse_advance son los nombres deprecados de la 1.0 (delegan en los nuevos y emiten DeprecationWarning). Desde la consola: compense simulate transfer|credit (ver La CLI compense).