Webhooks
Catálogo de eventos
Los 52 eventos que puedes suscribir, por categoría; el sobre común (event, version, tenant_id, occurred_at, resource), los eventos clásicos sin sobre, los montos como texto, el alias legado y ejemplos de payload.
En esta página
Cada evento tiene una clave (credits.decision, wallets.topup.completed, pqrs.responded…)
que usas al suscribir un endpoint y que llega en la cabecera X-Webhook-Event y en el campo event
del cuerpo. Esta página es una foto del catálogo al 1 de noviembre de 2026 (52 eventos). La fuente
de verdad es el catálogo en vivo,
GET/v1/gateway/webhooks/event-catalog/
: si aparece un evento nuevo, lo
verás primero ahí, con su payload_example.
El sobre de los eventos #
Los eventos namespaced (todas las categorías salvo general) llegan con el mismo sobre:
{
"event": "wallets.topup.completed",
"version": "1",
"tenant_id": "nova",
"occurred_at": "2026-10-27T15:00:00Z",
"resource": { "…": "campos propios del evento" }
}event: la clave del catálogo. Es la que debes usar para enrutar.version: versión mayor del esquema deresource, como texto. Agregar un campo opcional no la cambia, así que tu código debe ignorar los campos que no conoce. Solo un cambio incompatible sube la versión (ver Deprecaciones).tenant_id: código de tu organización.occurred_at: cuándo ocurrió el hecho, en UTC. Úsalo para ordenar: las entregas no llegan necesariamente en orden.resource: los datos del evento. Su forma exacta está en elpayload_examplede cada evento.
Montos como texto #
Todos los montos viajan como texto decimal ("amount": "20000.00"), nunca como número de punto
flotante. Conviértelos con un tipo decimal (Decimal en Python, decimal en C#, big.Rat o
shopspring/decimal en Go, bcmath en PHP) y no con float. Más en Montos.
Privacidad #
Los payloads se diseñan para viajar por internet:
- Billetera: nunca incluyen PAN, CVV ni el documento completo; los teléfonos van enmascarados
(
phone_number_masked) y las tarjetas solo traen los últimos 4 dígitos. - Salud:
resourcenunca lleva el documento completo ni el nombre del paciente. - PQRS: no lleva el texto de la respuesta ni el documento; consulta el detalle por API.
- Administrativo: sin diagnósticos ni datos bancarios.
Eventos clásicos (categoría general) #
Los eventos del ciclo de crédito clásico (application.*, credit.*, compliance.* e
installment.due_soon) son anteriores al sobre: no traen resource y sus campos vienen en el
nivel superior del cuerpo. Con los SDK léelos desde el cuerpo completo:
| SDK | Sobre namespaced | Evento clásico |
|---|---|---|
| Python | event.resource["decision"] | event.payload["score"] |
| JavaScript / TS | event.resource.decision | event.payload.score |
| PHP | $event->resource()['decision'] | $event->payload['score'] |
| .NET | evt.Resource["decision"] | evt.Payload["score"] |
| Go | ev.Resource()["decision"] | ev.Payload["score"] |
(score es solo ilustrativo: consulta el payload_example de cada evento clásico en el catálogo.)
Alias legado #
subsidy.application.received es el nombre antiguo de subsidies.application.received. El gateway
lo sigue aceptando al suscribir y lo normaliza a la clave canónica, que es la que verás en tus
endpoints y en las entregas. Los SDK traen el mapa de alias y una función para normalizar:
canonical_event_type (Python), canonicalEventType (JS), Webhook::canonicalEventType (PHP),
CompenseWebhooks.CanonicalEventType (.NET) y webhook.CanonicalEventType (Go).
El evento de prueba #
test.ping no está en el catálogo (no se suscribe): es el tipo que reciben tus endpoints cuando pides
una prueba sin event_type. Las pruebas traen "test": true y un event_id
que empieza por test-.
Catálogo completo #
Créditos #
| Evento | Desde | Cuándo se emite |
|---|---|---|
credits.application.received | 2026-06-01 | Se radicó una solicitud de crédito en el gateway y quedó aceptada para proceso. |
credits.decision | 2026-06-01 | Un asesor o el motor resolvió la solicitud (decision, approved o rejected). |
credits.disbursed | 2026-06-01 | Se desembolsaron los fondos del crédito aprobado. |
Subsidios #
| Evento | Desde | Cuándo se emite |
|---|---|---|
subsidies.application.received | 2026-06-01 | Se radicó una solicitud de subsidio (alias legado subsidy.application.received). |
subsidies.decision | 2026-06-01 | Un asesor o el motor de reglas resolvió la solicitud de subsidio. |
subsidies.disbursed | 2026-06-01 | Se pagó un subsidio aprobado. |
Cuota monetaria #
| Evento | Desde | Cuándo se emite |
|---|---|---|
monetary_allowance.generated | 2026-06-01 | Se generó la cuota monetaria de un afiliado para un período. |
monetary_allowance.disbursed | 2026-06-01 | Se pagó una cuota monetaria generada. |
Afiliados #
| Evento | Desde | Cuándo se emite |
|---|---|---|
affiliates.synced | 2026-06-01 | Terminó una sincronización de afiliados (resumen de creados, actualizados y retirados). |
Billetera #
| Evento | Desde | Cuándo se emite |
|---|---|---|
wallets.cashin.initiated | 2026-07-21 | Se registró una intención de recarga; aún no se mueve dinero. |
wallets.cashin.confirmed | 2026-07-21 | El origen confirmó la recarga y se emitió dinero electrónico a la billetera. |
wallets.transfer.created | 2026-07-21 | Se creó una transferencia (P2P, Bre-B o entre bolsillos) y quedó asentada en el libro mayor. |
wallets.transfer.settled | 2026-07-21 | La transferencia quedó liquidada. |
wallets.transfer.failed | 2026-07-21 | El riel rechazó la transferencia; no se movió dinero. |
wallets.disbursement.credited | 2026-07-21 | Se abonó un subsidio o crédito directamente a la billetera. |
wallets.withdrawal.authorized | 2026-07-21 | Se autorizó un retiro en cajero (token de un solo uso y fondos retenidos). |
wallets.bill_payment.completed | 2026-07-21 | El titular pagó una factura de servicios y el facturador la aceptó. |
wallets.qr_payment.completed | 2026-07-21 | El titular pagó un QR interoperable (EMVCo). |
wallets.transaction.reversed | 2026-07-21 | Se reversó una transacción (reembolso). |
wallets.dispute.opened | 2026-07-21 | El titular abrió una disputa sobre una transacción. |
wallets.dispute.resolved | 2026-07-21 | Un operador resolvió la disputa (a favor, con reverso, o rechazada). |
wallets.card.issued | 2026-07-21 | Se emitió una tarjeta (solo viajan los últimos 4 dígitos). |
wallets.topup.completed | 2026-10-27 | Se asentó una recarga de celular (carrier, teléfono enmascarado, amount). |
Cartera (pagos y mora) #
| Evento | Desde | Cuándo se emite |
|---|---|---|
payment.received | 2026-10-27 | Se registró un pago del crédito (cuota, abono o pago total) con su canal y reference. |
payment.applied | 2026-10-27 | El pago se distribuyó por la prelación (capital, intereses, mora, seguros, impuestos). |
installment.overdue | 2026-10-27 | Una cuota pasó a mora (se emite una vez por cuota, con days_overdue). |
loan.paid_off | 2026-10-27 | El crédito quedó en cero (paz y salvo). |
Servicios de salud #
| Evento | Desde | Cuándo se emite |
|---|---|---|
appointment.booked | 2026-10-27 | Se agendó una cita por el gateway. |
appointment.cancelled | 2026-10-27 | Se canceló una cita y se liberó el cupo. |
appointment.rescheduled | 2026-10-27 | Se movió una cita a otro cupo (previous trae la fecha y hora anteriores). |
appointment.status_changed | 2026-10-27 | Se registró la asistencia (completed o no_show). |
health_billing.payment.registered | 2026-10-27 | Se aplicó un pago del pagador (EPS) a una factura de salud. |
Administrativo #
| Evento | Desde | Cuándo se emite |
|---|---|---|
admin.hr.novelty.received | 2026-10-27 | Se registró una novedad de talento humano. |
admin.payroll.novelty.received | 2026-10-27 | Se cargó una novedad de nómina. |
admin.accounting.entry.drafted | 2026-10-27 | Se creó un comprobante contable en borrador. |
admin.treasury.document.registered | 2026-10-27 | Se registró un recaudo, egreso o cuenta de cobro en borrador (kind). |
admin.treasury.statement.imported | 2026-10-27 | Se cargó un extracto bancario. |
PQRS #
| Evento | Desde | Cuándo se emite |
|---|---|---|
pqrs.created | 2026-10-27 | Se radicó una PQRS. |
pqrs.status_changed | 2026-10-27 | Cambió el estado de una PQRS (incluye pasar a resolved por una respuesta final). |
pqrs.responded | 2026-10-27 | La entidad registró una respuesta oficial. |
Ciclo de crédito clásico (categoría general, sin sobre namespaced) #
| Evento | Desde | Cuándo se emite |
|---|---|---|
application.submitted | — | El solicitante terminó y envió la solicitud. |
application.scored | — | Resultados de buró, scoring y reglas del motor de decisión. |
application.approved | — | Aprobada por un analista o automáticamente por el motor de reglas. |
application.rejected | — | Rechazada por el motor de riesgo, cumplimiento o un analista. |
application.manual_review_required | — | Enviada a la bandeja de revisión humana. |
credit.activated | — | El crédito quedó activo tras el desembolso. |
credit.disbursed | — | El proveedor confirmó la transferencia de los fondos. |
credit.restructured | — | Se aprobó un cambio de condiciones del crédito. |
credit.written_off | — | El crédito se castigó como pérdida irrecuperable. |
installment.due_soon | — | Una cuota vence en los próximos 5 días. |
compliance.exception_raised | — | Alerta SARLAFT, UIAF u otra excepción regulatoria. |
compliance.ros_submitted | — | Se envió un reporte de operación sospechosa (ROS) a la UIAF. |
Eventos compartidos entre cartera y el ciclo clásico
payment.received, payment.applied e installment.overdue aparecen en la categoría loans
(cartera, desde el 27 de octubre de 2026) y comparten nombre con el ciclo de crédito clásico.
Suscribe la clave una vez; el payload_example del catálogo te dice qué forma tiene.
Lee el payload_example en vivo #
Antes de escribir el manejador de un evento, pide su ejemplo al catálogo: es exactamente la forma que llegará a tu receptor.
import json
import os
from compense_gateway import CompenseClient
client = CompenseClient(api_key=os.environ["COMPENSE_API_KEY"], environment="sandbox")
catalogo = client.webhooks.event_catalog(category="loans")
ejemplo = next(e for e in catalogo["events"] if e["event"] == "payment.received")
print(json.dumps(ejemplo["payload_example"], indent=2, ensure_ascii=False))import { CompenseClient } from "@compense/sdk";
const client = new CompenseClient({ apiKey: process.env.COMPENSE_API_KEY!, environment: "sandbox" });
const catalogo = await client.webhooks.eventCatalog({ category: "loans" });
const ejemplo = (catalogo.events as Array<Record<string, unknown>>).find((e) => e.event === "payment.received");
console.log(JSON.stringify(ejemplo?.payload_example, null, 2));<?php
require __DIR__ . '/vendor/autoload.php';
use Compense\Sdk\CompenseClient;
$client = new CompenseClient(apiKey: getenv('COMPENSE_API_KEY'), environment: 'sandbox');
$catalogo = $client->webhooks->eventCatalog(category: 'loans');
foreach ($catalogo['events'] as $e) {
if ($e['event'] === 'payment.received') {
echo json_encode($e['payload_example'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), PHP_EOL;
}
}using System.Linq;
using System.Text.Json.Nodes;
using Compense.Sdk;
using var client = new CompenseClient(
Environment.GetEnvironmentVariable("COMPENSE_API_KEY")!, CompenseEnvironment.Sandbox);
JsonObject catalogo = await client.Webhooks.EventCatalogAsync("loans");
JsonNode? ejemplo = catalogo["events"]!.AsArray()
.FirstOrDefault(e => e!["event"]?.GetValue<string>() == "payment.received");
Console.WriteLine(ejemplo?["payload_example"]?.ToJsonString());catalogo, err := client.Webhooks.EventCatalog(ctx, "loans")
if err != nil {
log.Fatal(err)
}
for _, raw := range catalogo["events"].([]any) {
ev := raw.(map[string]any)
if ev["event"] == "payment.received" {
out, _ := json.MarshalIndent(ev["payload_example"], "", " ")
fmt.Println(string(out))
}
}curl -s "https://sandbox.dev.compense.co/gw/v1/gateway/webhooks/event-catalog/?category=loans" \
-H "X-API-Key: $COMPENSE_API_KEY" \
| jq '.events[] | select(.event == "payment.received") | .payload_example'En el sandbox el catálogo es de muestra
Con una llave de sandbox, el catálogo devuelve 9 eventos representativos marcados con
"sample": true y un payload_example genérico. Para ver el catálogo completo y los ejemplos reales
usa una llave de producción en el ambiente dev. Ver Probar tus webhooks.
Ejemplos de payload #
wallets.topup.completed (billetera) #
Se emite cuando una recarga de celular queda asentada, en la misma transacción del movimiento.
{
"event": "wallets.topup.completed",
"version": "1",
"tenant_id": "nova",
"occurred_at": "2026-10-27T15:00:00Z",
"resource": {
"transaction_id": "c3d4e5f6-0000-4a1b-9c2d-000000000123",
"wallet_id": "7a8b9c0d-0000-4a1b-9c2d-000000000045",
"carrier": "CLARO",
"phone_number_masked": "******4567",
"amount": "20000.00",
"currency": "COP",
"status": "completed"
}
}Los demás eventos de billetera documentan estos campos en resource:
| Evento | Campos de resource |
|---|---|
wallets.transfer.created | transaction_id, source_wallet_id, recipient_wallet_id (null si es a otro banco), amount, currency, rail_reference, is_internal, status; Bre-B añade rail: "breb" |
wallets.transfer.failed | transaction_id, source_wallet_id, amount, currency, rail_reference, reason, status |
wallets.bill_payment.completed | transaction_id, wallet_id, service_id, amount, currency, provider, provider_reference, status |
payment.received (cartera) #
Se emite cuando se registra un pago de un crédito (cuota, abono o pago total). Su event_id es
determinista por recibo: un reintento de tu petición nunca produce un segundo webhook del mismo pago.
{
"event": "payment.received",
"version": "1",
"tenant_id": "horizonte",
"occurred_at": "2026-10-28T14:05:00Z",
"resource": {
"credit_id": "5d6e7f80-0000-4a1b-9c2d-000000000777",
"loan_number": "CRD-00123",
"payment_id": "9e0f1a2b-0000-4a1b-9c2d-000000000901",
"receipt_id": "7c8d9e0f-0000-4a1b-9c2d-000000000902",
"affiliate_id": "1b2c3d4e-0000-4a1b-9c2d-000000000310",
"kind": "installment",
"channel": "ACH_LIBRANZA",
"reference": "HORIZONTE-REC-2026-000901",
"amount": "456789.00",
"value_date": "2026-10-28",
"received_at": "2026-10-28T14:05:00Z"
}
}| Evento de cartera | Campos de resource |
|---|---|
payment.applied | applied_to_capital, applied_to_interest, applied_to_penalties, applied_to_insurance, applied_to_taxes, installments[], outstanding_principal_after, loan_status_after |
loan.paid_off | credit_id, loan_number, affiliate_id, last_payment_id, channel, paid_off_at |
installment.overdue | credit_id, loan_number, affiliate_id, installment_number, amount, due_date, days_overdue (una vez por cuota) |
pqrs.responded (PQRS) #
Se emite cuando la entidad registra una respuesta oficial, venga de la app, del panel o del API.
{
"event": "pqrs.responded",
"version": "1",
"tenant_id": "horizonte",
"occurred_at": "2026-10-29T16:20:00Z",
"resource": {
"pqrs_id": "4f0c1d2e-0000-4a1b-9c2d-000000000123",
"radicado": "PQRS-2026-000123",
"response_id": "9a1e2b3c-0000-4a1b-9c2d-000000000456",
"is_final": true,
"status": "resolved",
"external_reference": "HORIZONTE-CRM-88231",
"source": "gateway",
"actor": "integration:<llave> agent:ana.p"
}
}pqrs.created trae además issue_type, service_area, status, priority, origin_channel,
affiliate_id y deadline; pqrs.status_changed trae from_status, to_status y priority.
Las guías de cada módulo documentan sus eventos: billetera, cartera, salud, administrativo y PQRS.
Enruta por tipo de evento #
Un mismo endpoint puede recibir muchos eventos: enruta por event y deja pasar (con 2xx) los que
no manejas, para que un evento nuevo no te genere reintentos. Los SDK traen la foto del catálogo para
validaciones rápidas (EVENT_TYPES en Python, JS y PHP, CompenseWebhooks.EventTypes en .NET y
webhook.EventTypes() / webhook.IsKnownEventType() en Go), pero nunca rechazan un evento que no
conocen.
from compense_gateway.webhooks import WebhookEvent
def manejar(event: WebhookEvent) -> None:
if event.event == "wallets.topup.completed":
registrar_recarga(event.resource["transaction_id"], event.resource["amount"])
elif event.event == "payment.received":
conciliar_pago(event.resource["receipt_id"], event.resource["amount"])
elif event.event == "pqrs.responded":
actualizar_caso(event.resource["radicado"], event.resource["status"])
# cualquier otro evento: ignóralo y responde 2xx igualimport type { WebhookEvent } from "@compense/sdk/webhooks";
function manejar(event: WebhookEvent): void {
switch (event.event) {
case "wallets.topup.completed":
registrarRecarga(String(event.resource.transaction_id), String(event.resource.amount));
break;
case "payment.received":
conciliarPago(String(event.resource.receipt_id), String(event.resource.amount));
break;
case "pqrs.responded":
actualizarCaso(String(event.resource.radicado), String(event.resource.status));
break;
default:
break; // cualquier otro evento: ignóralo y responde 2xx igual
}
}use Compense\Sdk\Webhook\WebhookEvent;
function manejar(WebhookEvent $event): void
{
$r = $event->resource();
match ($event->event()) {
'wallets.topup.completed' => registrarRecarga($r['transaction_id'], $r['amount']),
'payment.received' => conciliarPago($r['receipt_id'], $r['amount']),
'pqrs.responded' => actualizarCaso($r['radicado'], $r['status']),
default => null, // cualquier otro evento: ignóralo y responde 2xx igual
};
}using Compense.Sdk.Webhooks;
static void Manejar(WebhookEvent evt)
{
switch (evt.Event)
{
case "wallets.topup.completed":
RegistrarRecarga(evt.Resource["transaction_id"]!.GetValue<string>(), evt.Resource["amount"]!.GetValue<string>());
break;
case "payment.received":
ConciliarPago(evt.Resource["receipt_id"]!.GetValue<string>(), evt.Resource["amount"]!.GetValue<string>());
break;
case "pqrs.responded":
ActualizarCaso(evt.Resource["radicado"]!.GetValue<string>(), evt.Resource["status"]!.GetValue<string>());
break;
default:
break; // cualquier otro evento: ignóralo y responde 2xx igual
}
}func manejar(ev *webhook.Event) {
r := ev.Resource()
switch ev.Event() {
case "wallets.topup.completed":
registrarRecarga(fmt.Sprint(r["transaction_id"]), fmt.Sprint(r["amount"]))
case "payment.received":
conciliarPago(fmt.Sprint(r["receipt_id"]), fmt.Sprint(r["amount"]))
case "pqrs.responded":
actualizarCaso(fmt.Sprint(r["radicado"]), fmt.Sprint(r["status"]))
default:
// cualquier otro evento: ignóralo y responde 2xx igual
}
}# Con cURL no hay receptor: ve el tipo de cada entrega reciente en el historial.
curl -s "https://sandbox.dev.compense.co/gw/v1/gateway/webhooks/deliveries/?page_size=20" \
-H "X-API-Key: $COMPENSE_API_KEY" | jq -r '.results[] | "\(.event_type)\t\(.status)"'El receptor completo (verificación de firma, deduplicación y respuesta rápida) está en Verificar la firma.