Guía de implementación
Transacciones y sistema
Consulta el estado asíncrono de cualquier escritura con client.transactions.get (status, history, reference) y valida la llave y sus scopes con client.system.health, también como health check de monitoreo. Con trace_id y X-Request-ID para soporte.
En esta página
Dos operaciones transversales que no pertenecen a ningún módulo de negocio, pero que toda integración usa:
| Operación | Método SDK (Python) | Scope | Para qué |
|---|---|---|---|
GET/v1/gateway/transactions/{tx_id}/ | client.transactions.get(tx_id) | ninguno (cualquier llave válida) | Estado y traza de una escritura que hiciste por el gateway. |
GET/v1/gateway/health/ | client.system.health() | ninguno | Validar la llave: tenant y scopes. Health check de tu monitoreo. |
En los demás SDK: client.transactions.get / client.system.health() (TypeScript),
$client->transactions->get / $client->system->health() (PHP), client.Transactions.GetAsync /
client.System.HealthAsync() (.NET) y client.Transactions.Get / client.System.Health (Go).
Estado de una transacción #
Cada escritura que pasa por el gateway (radicar, decidir, desembolsar, registrar un recaudo, cargar
afiliados…) deja un registro de transacción y te devuelve su transaction_id en el sobre de
respuesta.
GET/v1/gateway/transactions/{tx_id}/
lee ese registro.
| Campo | Modelo SDK | Qué es |
|---|---|---|
transaction_id | sí | El id que te devolvió la escritura. |
module | sí | credits, subsidies, affiliates, loans, wallet… |
status | sí | PROCESSING, COMPLETED o FAILED (ver abajo). |
reference | sí | La referencia de negocio que quedó asociada: el application_id en créditos y subsidios, el disbursement_id en cuota monetaria, el receipt_id en cartera. |
history | sí | Los hitos por los que pasó la llamada, en orden (p. ej. ["received", "submitted"]). |
operation | en raw | La operación interna (application, decision:<id>, disbursement:<id>…). |
data | en raw | La respuesta que se guardó (la misma data del sobre). |
error | en raw | El motivo, si status es FAILED. |
created_at, updated_at | en raw | Marcas de tiempo ISO 8601. |
Los modelos tipados (TransactionStatus) exponen los cinco primeros; el resto está en raw
(status.raw, $status->raw, status.Raw).
Qué significa cada estado #
stateDiagram-v2 [*] --> PROCESSING: el gateway acepta la escritura PROCESSING --> COMPLETED: el motor la completó PROCESSING --> FAILED: el motor la rechazó o falló PROCESSING --> PROCESSING: la operación de negocio sigue en curso
PROCESSING: la escritura se aceptó y la operación de negocio sigue abierta. Es el estado normal de una radicación de crédito o subsidio (la solicitud queda en manos del motor y del analista) y de una carga de afiliados con filas rechazadas.COMPLETED: la operación terminó (una decisión, un desembolso, un recaudo aplicado, una carga sin rechazos).FAILED: el motor rechazó la petición o no respondió;raw.errortrae el motivo.
El registro describe la llamada al gateway, no la vida completa del objeto de negocio: el estado
de una solicitud de crédito (aprobada, desembolsada…) se consulta en su módulo
(credits.get_application) y llega por
webhooks. Usa transactions.get para soporte, auditoría y conciliación de tus
propias llamadas.
status = client.transactions.get("a1b2c3d4-0000-4a1b-9c2d-000000000001")
print(status.module, status.status, status.reference, status.history)
print(status.raw.get("operation"), status.raw.get("error"))const status = await client.transactions.get("a1b2c3d4-0000-4a1b-9c2d-000000000001");
console.log(status.module, status.status, status.reference, status.history);
console.log(status.raw.operation, status.raw.error);$status = $client->transactions->get('a1b2c3d4-0000-4a1b-9c2d-000000000001');
echo $status->module, ' ', $status->status, ' ', $status->reference ?? '-', ' ', implode(' → ', $status->history), PHP_EOL;
echo $status->raw['operation'] ?? '-', PHP_EOL;TransactionStatus status = await client.Transactions.GetAsync("a1b2c3d4-0000-4a1b-9c2d-000000000001");
Console.WriteLine($"{status.Module} {status.Status} {status.Reference} {string.Join(" → ", status.History)}");
Console.WriteLine($"{status.Raw["operation"]} {status.Raw["error"]}");status, err := client.Transactions.Get(ctx, "a1b2c3d4-0000-4a1b-9c2d-000000000001")
if err != nil {
log.Fatal(err)
}
fmt.Println(status.Module, status.Status, status.History, status.Raw["operation"])
if status.Reference != nil {
fmt.Println("Referencia:", *status.Reference)
}curl https://sandbox.dev.compense.co/gw/v1/gateway/transactions/a1b2c3d4-0000-4a1b-9c2d-000000000001/ \
-H "X-API-Key: $COMPENSE_API_KEY"{
"transaction_id": "a1b2c3d4-0000-4a1b-9c2d-000000000001",
"module": "credits",
"operation": "application",
"status": "PROCESSING",
"reference": "sbx-E-2cf1b69c533d",
"history": ["received", "submitted"],
"data": {"application_id": "sbx-E-2cf1b69c533d", "status": "RECEIVED"},
"error": null,
"created_at": "2026-11-02T14:03:11.204Z",
"updated_at": "2026-11-02T14:03:11.612Z"
}Solo ves las transacciones de tu entidad: un transaction_id de otro tenant (o inexistente)
responde 404.
Esperar un resultado sin webhooks #
Lo recomendado es reaccionar a los webhooks, pero si un proceso por lotes necesita
confirmar sus propias escrituras puedes consultar con espera creciente hasta que el estado deje de
ser PROCESSING. Ten presente que una radicación se queda en PROCESSING a propósito: espera así
solo operaciones que terminan (decisiones, desembolsos, recaudos, cargas).
import time
def esperar(tx_id: str, intentos: int = 6) -> str:
espera = 1.0
for _ in range(intentos):
st = client.transactions.get(tx_id)
if st.status != "PROCESSING":
return st.status
time.sleep(espera)
espera *= 2
return "PROCESSING"async function esperar(txId: string, intentos = 6): Promise<string> {
let espera = 1000;
for (let i = 0; i < intentos; i++) {
const st = await client.transactions.get(txId);
if (st.status !== "PROCESSING") return st.status;
await new Promise((r) => setTimeout(r, espera));
espera *= 2;
}
return "PROCESSING";
}function esperar(\Compense\Sdk\CompenseClient $client, string $txId, int $intentos = 6): string
{
$espera = 1;
for ($i = 0; $i < $intentos; $i++) {
$st = $client->transactions->get($txId);
if ($st->status !== 'PROCESSING') {
return $st->status;
}
sleep($espera);
$espera *= 2;
}
return 'PROCESSING';
}async Task<string> EsperarAsync(CompenseClient client, string txId, int intentos = 6)
{
var espera = TimeSpan.FromSeconds(1);
for (int i = 0; i < intentos; i++)
{
TransactionStatus st = await client.Transactions.GetAsync(txId);
if (st.Status != "PROCESSING") return st.Status;
await Task.Delay(espera);
espera *= 2;
}
return "PROCESSING";
}func esperar(ctx context.Context, client *compense.Client, txID string, intentos int) (string, error) {
espera := time.Second
for i := 0; i < intentos; i++ {
st, err := client.Transactions.Get(ctx, txID)
if err != nil {
return "", err
}
if st.Status != "PROCESSING" {
return st.Status, nil
}
time.Sleep(espera)
espera *= 2
}
return "PROCESSING", nil
}for espera in 1 2 4 8 16 32; do
ESTADO=$(curl -s https://sandbox.dev.compense.co/gw/v1/gateway/transactions/$TX_ID/ \
-H "X-API-Key: $COMPENSE_API_KEY" | jq -r .status)
[ "$ESTADO" != "PROCESSING" ] && break
sleep $espera
done
echo "$ESTADO"¿Y si nunca recibiste el transaction_id?
Si la escritura se cortó antes de llegar la respuesta, no tienes transaction_id que consultar.
Reenvía la misma petición con la misma Idempotency-Key: el gateway te devuelve la respuesta
original (con su transaction_id e idempotent_replay: true) o, si sigue en curso, un 409 que
puedes reintentar. Ver Idempotencia.
Validar la llave y sus scopes #
GET/v1/gateway/health/
no toca el motor: confirma que la llave es válida y devuelve el tenant
al que pertenece y la lista de scopes. Úsalo en dos momentos:
- Al arrancar tu servicio: verifica que la llave tiene todos los scopes que tu integración
necesita y falla rápido (con un mensaje claro) si falta alguno, en vez de descubrirlo con un
403en la primera operación de un afiliado. - Como health check de monitoreo: un
200dice que el gateway responde y que tu llave sigue vigente (no fue revocada ni rotada).
from compense_gateway import AuthError
REQUERIDOS = {"credits:submit", "credits:read", "loans:read", "loans:collect"}
try:
health = client.system.health()
except AuthError as e:
raise SystemExit(f"Llave inválida o revocada ({e.status}, trace_id={e.trace_id})")
faltan = REQUERIDOS - set(health.scopes)
if faltan:
raise SystemExit(f"A la llave de {health.tenant} le faltan scopes: {sorted(faltan)}")
print("OK", health.tenant, health.status)import { AuthError } from "@compense/sdk";
const REQUERIDOS = ["credits:submit", "credits:read", "loans:read", "loans:collect"];
try {
const health = await client.system.health();
const faltan = REQUERIDOS.filter((s) => !health.scopes.includes(s));
if (faltan.length) throw new Error(`A la llave de ${health.tenant} le faltan scopes: ${faltan.join(", ")}`);
console.log("OK", health.tenant, health.status);
} catch (err) {
if (err instanceof AuthError) throw new Error(`Llave inválida o revocada (${err.status}, trace_id=${err.traceId})`);
throw err;
}use Compense\Sdk\Exception\AuthException;
$requeridos = ['credits:submit', 'credits:read', 'loans:read', 'loans:collect'];
try {
$health = $client->system->health();
} catch (AuthException $e) {
exit("Llave inválida o revocada ({$e->status}, trace_id={$e->traceId})\n");
}
$faltan = array_diff($requeridos, $health->scopes);
if ($faltan) {
exit("A la llave de {$health->tenant} le faltan scopes: " . implode(', ', $faltan) . "\n");
}
echo 'OK ', $health->tenant, PHP_EOL;string[] requeridos = { "credits:submit", "credits:read", "loans:read", "loans:collect" };
GatewayHealth health;
try
{
health = await client.System.HealthAsync();
}
catch (AuthException e)
{
throw new InvalidOperationException($"Llave inválida o revocada ({e.Status}, trace_id={e.TraceId})");
}
var faltan = requeridos.Except(health.Scopes).ToList();
if (faltan.Count > 0)
{
throw new InvalidOperationException($"A la llave de {health.Tenant} le faltan scopes: {string.Join(", ", faltan)}");
}
Console.WriteLine($"OK {health.Tenant} {health.Status}");requeridos := []string{"credits:submit", "credits:read", "loans:read", "loans:collect"}
health, err := client.System.Health(ctx)
if compense.IsAuthError(err) {
log.Fatalf("llave inválida o revocada: %v", err)
} else if err != nil {
log.Fatal(err)
}
tiene := map[string]bool{}
for _, s := range health.Scopes {
tiene[s] = true
}
for _, s := range requeridos {
if !tiene[s] {
log.Fatalf("a la llave de %s le falta el scope %s", health.Tenant, s)
}
}
fmt.Println("OK", health.Tenant, health.Status)curl -s https://sandbox.dev.compense.co/gw/v1/gateway/health/ \
-H "X-API-Key: $COMPENSE_API_KEY" | jq '{tenant, scopes}'
# {"tenant": "horizonte", "scopes": ["affiliates:read", "credits:read", "credits:submit", ...]}| Respuesta | Significado |
|---|---|
200 | Llave válida: revisa tenant y scopes. |
401 | Falta la cabecera X-API-Key. |
403 | Llave inválida, revocada, de otro entorno (una llave de sandbox contra producción o al revés) o una IP fuera de la lista permitida de la llave (title: IP not allowed). |
Para monitoreo, una consulta por minuto es suficiente. Un 200 no garantiza que el motor de un
módulo esté arriba (este endpoint no lo toca): para eso vigila los 502/504 de tus operaciones
reales y los límites de tasa de tus escrituras.
trace_id y X-Request-ID #
Cada respuesta del gateway lleva la cabecera X-Request-ID. En los errores, el cuerpo
application/problem+json trae el mismo valor como trace_id: es lo que debes citar a soporte,
porque con él se encuentra la causa exacta en los registros del gateway (que nunca se filtra en la
respuesta).
- Los SDK generan un
X-Request-IDpor cada operación lógica y lo mantienen igual en todos sus reintentos automáticos; en las excepciones lo exponen comotrace_id(Python),traceId(TypeScript, PHP),TraceId(.NET) yTraceID(Go). - Con cURL o tu propio cliente HTTP puedes enviar tu propio
X-Request-ID(por ejemplo, el id de la petición en tu sistema) y el gateway lo devuelve. - En una respuesta exitosa tu llave de correlación es el
transaction_iddel sobre.
Registra siempre juntos transaction_id, trace_id (si hubo error), tu external_reference y la
Idempotency-Key: con esos cuatro datos cualquier caso de soporte se resuelve en minutos. Más en
Errores y Soporte.