Sandbox
Simuladores de ciclo de vida
Avanza una transferencia de billetera (PENDING → PROCESSING → SETTLED) o un desembolso de crédito (PENDING → PROCESSING → DISBURSED) un estado a la vez, y llega a FAILED o RETURNED con los montos mágicos 22.22 y 33.33.
En esta página
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:
| Simulador | Operación | Ciclo feliz |
|---|---|---|
| Transferencia de billetera | POST/v1/gateway/sandbox/wallet/simulate/ | PENDING → PROCESSING → SETTLED |
| Desembolso de crédito | POST/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”):
| Monto | Transferencia de billetera | Desembolso de crédito |
|---|---|---|
22.22 | PENDING → PROCESSING → FAILED | PENDING → PROCESSING → FAILED |
33.33 | PENDING → PROCESSING → SETTLED → RETURNED | PENDING → PROCESSING → DISBURSED → RETURNED |
11.11 o cualquier otro | PENDING → PROCESSING → SETTLED | PENDING → 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 asumePENDING(y respondePROCESSING). - Un
statusque 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 comoreference, ú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 con | Ciclo del desembolso | reason_code |
|---|---|---|
CC 1000000001 (elegible), monto ≤ 50.000.000 | Según el monto mágico | DISBURSEMENT_FAILED / DISBURSEMENT_RETURNED si aplica |
CC 1000000001, monto > 50.000.000 | PENDING → PROCESSING → FAILED | AMOUNT_EXCEEDS_LIMIT |
CC 1000000002 (inelegible) | PENDING → PROCESSING → FAILED | NOT_ELIGIBLE |
CC 1000000003 (revisión manual) | PENDING → PROCESSING → FAILED | MANUAL_REVIEW |
CC 1000000004 (con deudas) | PENDING → PROCESSING → FAILED | OUTSTANDING_DEBT |
La respuesta #
{
"object": "wallet_transfer",
"previous_status": "PROCESSING",
"status": "FAILED",
"terminal": true,
"amount": 22.22,
"reference": "NOVA-P2P-2026-000123",
"reason_code": "TRANSFER_FAILED",
"environment": "sandbox"
}| Campo | Qué es |
|---|---|
object | wallet_transfer o disbursement. |
previous_status / status | El estado que enviaste y el siguiente. |
terminal | true cuando status es el final del ciclo: deja de llamar. |
amount | El monto que enviaste (aquí, a diferencia del resto del API, como número). |
reference | Tu transaction_id (o el id de la solicitud, en el desembolso). |
reason_code | Solo en FAILED o RETURNED: TRANSFER_FAILED, TRANSFER_RETURNED, DISBURSEMENT_FAILED, DISBURSEMENT_RETURNED o el motivo del perfil. |
profile | Solo 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.
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_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); // tu lógica para cada estado
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); // tu lógica para cada estado
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); // tu lógica para cada estado
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) // tu lógica para cada estado
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')
doneEl 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:
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 de la solicitud radicada (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')
doneCambia 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
FAILEDo unRETURNED. - 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 #
| SDK | Transferencia | Desembolso |
|---|---|---|
| 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{...}) |
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).