Webhooks
Suscribir un endpoint
Autogestión de webhooks por API: consulta el catálogo, registra una URL HTTPS pública, guarda el secreto que se muestra una sola vez, lista, edita, pausa y elimina endpoints, también desde la CLI compense.
En esta página
Un endpoint es una URL tuya que recibe eventos. Lo registras con tu llave de API, eliges a qué
eventos se suscribe y Compense te devuelve una sola vez el secreto con el que firmará cada entrega.
Todo lo que haces aquí queda ligado a la organización dueña de la llave: un id de otra organización
responde 404.
Scopes #
| Scope | Permite |
|---|---|
webhooks:read | Consultar el catálogo, listar y ver endpoints, listar y ver entregas. |
webhooks:manage | Crear, editar y eliminar endpoints, rotar el secreto, enviar pruebas y reintentar entregas. |
Una llave sin el scope recibe 403. Pide el menor scope que necesites: un sistema que solo recibe
eventos no necesita ninguno; uno que administra endpoints necesita webhooks:manage.
Firma de peticiones
webhooks:manage está en la lista de operaciones de alto valor. Si tu llave tiene activada la firma
de peticiones (X-Compense-Signature), estas escrituras también deben ir firmadas. Ver
Autenticación.
1. Consulta el catálogo de eventos #
GET/v1/gateway/webhooks/event-catalog/
devuelve todos los eventos suscribibles con su categoría,
descripción, version y un payload_example, más los alias legados y el contrato de firma. Es la
fuente de verdad: un evento nuevo aparece primero ahí. Filtra con category (credits,
subsidies, monetary_allowance, affiliates, wallets, loans, health, admin, pqrs,
general).
import os
from compense_gateway import CompenseClient
client = CompenseClient(api_key=os.environ["COMPENSE_API_KEY"], environment="sandbox")
catalogo = client.webhooks.event_catalog(category="wallets")
for ev in catalogo["events"]:
print(ev["event"], ev["category"], ev["version"])
print(catalogo["aliases"]) # {"subsidy.application.received": "subsidies.application.received"}import { CompenseClient } from "@compense/sdk";
const client = new CompenseClient({ apiKey: process.env.COMPENSE_API_KEY!, environment: "sandbox" });
const catalogo = await client.webhooks.eventCatalog({ category: "wallets" });
for (const ev of catalogo.events as Array<Record<string, unknown>>) {
console.log(ev.event, ev.category, ev.version);
}<?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: 'wallets');
foreach ($catalogo['events'] as $ev) {
echo $ev['event'], ' ', $ev['category'], ' ', $ev['version'], PHP_EOL;
}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("wallets");
foreach (JsonNode? ev in catalogo["events"]!.AsArray())
{
Console.WriteLine($"{ev!["event"]} {ev["category"]} {ev["version"]}");
}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)
}
catalogo, err := client.Webhooks.EventCatalog(ctx, "wallets") // "" = todas las categorías
if err != nil {
log.Fatal(err)
}
for _, raw := range catalogo["events"].([]any) {
ev := raw.(map[string]any)
fmt.Println(ev["event"], ev["category"], ev["version"])
}
}curl "https://sandbox.dev.compense.co/gw/v1/gateway/webhooks/event-catalog/?category=wallets" \
-H "X-API-Key: $COMPENSE_API_KEY"Respuesta (resumida):
{
"count": 14,
"categories": { "wallets": ["wallets.cashin.initiated", "wallets.cashin.confirmed", "…"] },
"events": [
{
"event": "wallets.topup.completed",
"label": "…",
"description": "…",
"category": "wallets",
"since": "2026-10-27",
"version": "1",
"payload_example": { "event": "wallets.topup.completed", "version": "1", "tenant_id": "…", "occurred_at": "…", "resource": { "…": "…" } }
}
],
"aliases": { "subsidy.application.received": "subsidies.application.received" },
"signature": { "algorithm": "HMAC-SHA256", "headers": { "X-Webhook-Signature": "sha256=<hex …>", "…": "…" } }
}La tabla completa, por categoría, está en Catálogo de eventos.
2. Registra el endpoint #
POST/v1/gateway/webhooks/endpoints/
registra una URL y la lista de eventos que quieres recibir.
Reglas de la URL
- Debe usar HTTPS y apuntar a un host público.
- Se rechazan
localhost,*.local,*.internal, las IP privadas, loopback y link-local (169.254.*), y las URL con credenciales embebidas (https://usuario:clave@…).
Reglas de events
- Lista no vacía de claves del catálogo.
- Los alias legados se normalizan a su clave canónica (
subsidy.application.receivedse guarda comosubsidies.application.received).
Límite: 20 endpoints por organización (422 WEBHOOK_ENDPOINT_LIMIT al pasarlo).
from compense_gateway import WebhookEndpoint
tx = client.webhooks.endpoints.create(
url="https://erp.nova.example/compense/webhooks",
events=["credits.decision", "wallets.topup.completed"],
label="ERP Nova",
description="Conciliación contable",
idempotency_key="nova-wh-erp-001",
)
endpoint = WebhookEndpoint.from_dict(tx.data)
print(endpoint.id, endpoint.status, endpoint.timeout_seconds, endpoint.max_retries)
secreto = tx.data["secret"] # SOLO viene aquí: guárdalo ya en tu gestor de secretosimport { WebhookEndpoint } from "@compense/sdk";
const tx = await client.webhooks.endpoints.create({
url: "https://erp.nova.example/compense/webhooks",
events: ["credits.decision", "wallets.topup.completed"],
label: "ERP Nova",
description: "Conciliación contable",
idempotencyKey: "nova-wh-erp-001",
});
const endpoint = WebhookEndpoint.fromDict(tx.data);
console.log(endpoint.id, endpoint.status, endpoint.timeoutSeconds, endpoint.maxRetries);
const secreto = endpoint.secret; // SOLO viene aquí: guárdalo ya en tu gestor de secretos
use Compense\Sdk\Model\WebhookEndpoint;
$tx = $client->webhooks->endpoints->create(
url: 'https://erp.nova.example/compense/webhooks',
events: ['credits.decision', 'wallets.topup.completed'],
label: 'ERP Nova',
description: 'Conciliación contable',
idempotencyKey: 'nova-wh-erp-001',
);
$endpoint = WebhookEndpoint::fromArray($tx->data);
echo $endpoint->id, ' ', $endpoint->status, PHP_EOL;
$secreto = $tx->data['secret']; // SOLO viene aquí: guárdalo ya en tu gestor de secretos
using Compense.Sdk.Models;
Transaction tx = await client.Webhooks.Endpoints.CreateAsync(
"https://erp.nova.example/compense/webhooks",
new[] { "credits.decision", "wallets.topup.completed" },
label: "ERP Nova",
description: "Conciliación contable",
idempotencyKey: "nova-wh-erp-001");
WebhookEndpoint endpoint = WebhookEndpoint.FromJson(tx.Data);
Console.WriteLine($"{endpoint.Id} {endpoint.Status} {endpoint.TimeoutSeconds} {endpoint.MaxRetries}");
string secreto = endpoint.Secret!; // SOLO viene aquí: guárdalo ya en tu gestor de secretostx, err := client.Webhooks.Endpoints.Create(ctx, &compense.WebhookEndpointCreateParams{
URL: "https://erp.nova.example/compense/webhooks",
Events: []string{"credits.decision", "wallets.topup.completed"},
Label: "ERP Nova",
Description: "Conciliación contable",
}, compense.WithIdempotencyKey("nova-wh-erp-001"))
if err != nil {
log.Fatal(err)
}
endpoint := compense.WebhookEndpointFromWire(tx.Data)
fmt.Println(endpoint.ID, endpoint.Status)
secreto := endpoint.Secret // SOLO viene aquí: guárdalo ya en tu gestor de secretos
_ = secretocurl -X POST https://sandbox.dev.compense.co/gw/v1/gateway/webhooks/endpoints/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: nova-wh-erp-001" \
-H "Content-Type: application/json" \
-d '{
"url": "https://erp.nova.example/compense/webhooks",
"events": ["credits.decision", "wallets.topup.completed"],
"label": "ERP Nova",
"description": "Conciliación contable",
"active": true
}'La respuesta es el sobre de escritura; el endpoint va en data:
{
"status": "success",
"transaction_id": "a1b2c3d4-0000-4a1b-9c2d-000000000010",
"module": "webhooks",
"data": {
"id": "3f1a2b3c-0000-4a1b-9c2d-000000000001",
"url": "https://erp.nova.example/compense/webhooks",
"label": "ERP Nova",
"events": ["credits.decision", "wallets.topup.completed"],
"active": true,
"status": "active",
"timeout_seconds": 10,
"max_retries": 5,
"stats": { "deliveries_count_24h": 0, "success_rate_24h": 100.0, "last_delivery_at": null },
"secret": "whsec_..."
}
}El secreto se muestra una sola vez
data.secret solo viene en la respuesta de crear y de rotar. El gateway no guarda secretos:
ninguna lectura lo devuelve, y si repites la creación con la misma Idempotency-Key recibes la
respuesta original con "idempotent_replay": true pero sin secret. Guárdalo en tu gestor de
secretos (HashiCorp Vault, Azure Key Vault, Google Secret Manager…) antes de hacer cualquier otra cosa. Si lo
perdiste, rótalo.
3. Lista y consulta tus endpoints #
GET/v1/gateway/webhooks/endpoints/
y
GET/v1/gateway/webhooks/endpoints/{endpoint_id}/
devuelven los endpoints de tu
organización sin secreto, con su estado y estadísticas de las últimas 24 horas.
for ep in client.webhooks.endpoints.list():
print(ep.id, ep.status, ep.url, ep.events)
ep = client.webhooks.endpoints.get("3f1a2b3c-0000-4a1b-9c2d-000000000001")
print(ep.raw["stats"]) # deliveries_count_24h, success_rate_24h, last_delivery_at…for (const ep of await client.webhooks.endpoints.list()) {
console.log(ep.id, ep.status, ep.url, ep.events);
}
const ep = await client.webhooks.endpoints.get("3f1a2b3c-0000-4a1b-9c2d-000000000001");
console.log(ep.raw.stats);foreach ($client->webhooks->endpoints->list() as $ep) {
echo $ep->id, ' ', $ep->status, ' ', $ep->url, ' ', implode(',', $ep->events), PHP_EOL;
}
$ep = $client->webhooks->endpoints->get('3f1a2b3c-0000-4a1b-9c2d-000000000001');
print_r($ep->raw['stats'] ?? []);foreach (WebhookEndpoint ep in await client.Webhooks.Endpoints.ListAsync())
{
Console.WriteLine($"{ep.Id} {ep.Status} {ep.Url} {string.Join(",", ep.Events)}");
}
WebhookEndpoint uno = await client.Webhooks.Endpoints.GetAsync("3f1a2b3c-0000-4a1b-9c2d-000000000001");
Console.WriteLine(uno.Raw["stats"]);endpoints, err := client.Webhooks.Endpoints.List(ctx)
if err != nil {
log.Fatal(err)
}
for _, ep := range endpoints {
fmt.Println(ep.ID, ep.Status, ep.URL, ep.Events)
}
uno, err := client.Webhooks.Endpoints.Get(ctx, "3f1a2b3c-0000-4a1b-9c2d-000000000001")
if err != nil {
log.Fatal(err)
}
fmt.Println(uno.Raw["stats"])curl https://sandbox.dev.compense.co/gw/v1/gateway/webhooks/endpoints/ \
-H "X-API-Key: $COMPENSE_API_KEY"
curl https://sandbox.dev.compense.co/gw/v1/gateway/webhooks/endpoints/$ENDPOINT_ID/ \
-H "X-API-Key: $COMPENSE_API_KEY"4. Edita o pausa un endpoint #
PATCH/v1/gateway/webhooks/endpoints/{endpoint_id}/
es una actualización parcial: solo acepta url, events,
label, description y active. Cualquier otro campo, incluido secret, responde
422 WEBHOOK_INVALID_FIELD (el secreto se cambia rotándolo).
Para dejar de recibir eventos sin perder el historial, pausa con active: false; para volver,
active: true.
tx = client.webhooks.endpoints.update(
"3f1a2b3c-0000-4a1b-9c2d-000000000001",
events=["credits.decision", "credits.disbursed"],
active=False, # pausar
idempotency_key="nova-wh-erp-001-pausa",
)
print(tx.data["status"]) # pausedconst tx = await client.webhooks.endpoints.update("3f1a2b3c-0000-4a1b-9c2d-000000000001", {
events: ["credits.decision", "credits.disbursed"],
active: false, // pausar
idempotencyKey: "nova-wh-erp-001-pausa",
});
console.log(tx.data.status); // paused
$tx = $client->webhooks->endpoints->update(
'3f1a2b3c-0000-4a1b-9c2d-000000000001',
events: ['credits.decision', 'credits.disbursed'],
active: false, // pausar
idempotencyKey: 'nova-wh-erp-001-pausa',
);
echo $tx->data['status'], PHP_EOL; // paused
Transaction pausa = await client.Webhooks.Endpoints.UpdateAsync(
"3f1a2b3c-0000-4a1b-9c2d-000000000001",
events: new[] { "credits.decision", "credits.disbursed" },
active: false, // pausar
idempotencyKey: "nova-wh-erp-001-pausa");
Console.WriteLine(pausa.Data["status"]); // pausedpausa, err := client.Webhooks.Endpoints.Update(ctx, "3f1a2b3c-0000-4a1b-9c2d-000000000001",
&compense.WebhookEndpointUpdateParams{
Events: []string{"credits.decision", "credits.disbursed"},
Active: compense.Bool(false), // pausar
}, compense.WithIdempotencyKey("nova-wh-erp-001-pausa"))
if err != nil {
log.Fatal(err)
}
fmt.Println(pausa.Data["status"]) // pausedcurl -X PATCH https://sandbox.dev.compense.co/gw/v1/gateway/webhooks/endpoints/$ENDPOINT_ID/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: nova-wh-erp-001-pausa" \
-H "Content-Type: application/json" \
-d '{"events": ["credits.decision", "credits.disbursed"], "active": false}'Un endpoint disabled (deshabilitado por Compense) no se reactiva con active: true: responde
409 WEBHOOK_ENDPOINT_DISABLED. Crea otro endpoint o escribe a soporte.
5. Elimina un endpoint #
DELETE/v1/gateway/webhooks/endpoints/{endpoint_id}/
elimina el endpoint y todo su historial de entregas. No se
puede deshacer.
Eliminar borra el historial
Si solo quieres dejar de recibir eventos (por ejemplo, durante una ventana de mantenimiento), usa
active: false. Así conservas las entregas para auditar y reintentar.
tx = client.webhooks.endpoints.delete("3f1a2b3c-0000-4a1b-9c2d-000000000001")
print(tx.status)const borrado = await client.webhooks.endpoints.delete("3f1a2b3c-0000-4a1b-9c2d-000000000001");
console.log(borrado.status);$tx = $client->webhooks->endpoints->delete('3f1a2b3c-0000-4a1b-9c2d-000000000001');
echo $tx->status, PHP_EOL;Transaction borrado = await client.Webhooks.Endpoints.DeleteAsync("3f1a2b3c-0000-4a1b-9c2d-000000000001");
Console.WriteLine(borrado.Status);borrado, err := client.Webhooks.Endpoints.Delete(ctx, "3f1a2b3c-0000-4a1b-9c2d-000000000001")
if err != nil {
log.Fatal(err)
}
fmt.Println(borrado.Status)curl -X DELETE https://sandbox.dev.compense.co/gw/v1/gateway/webhooks/endpoints/$ENDPOINT_ID/ \
-H "X-API-Key: $COMPENSE_API_KEY" \
-H "Idempotency-Key: nova-wh-erp-001-borrar"Los SDK generan una Idempotency-Key automáticamente en cada escritura si no le das una; en cURL
envíala tú (ver Idempotencia).
Estados de un endpoint #
| Estado | Quién lo pone | Recibe eventos | Qué hacer |
|---|---|---|---|
active | Tú (al crear o con active: true) | Sí | Nada. |
paused | Tú (active: false) | No | Reactívalo con active: true cuando quieras. |
failing | Compense: con 10 o más entregas en 24 h, menos del 50 % fue exitosa | Sí | Revisa tu receptor y el historial de entregas. |
disabled | Compense (p. ej. tras 3 respuestas 410 Gone) | No | No se reactiva por API: crea otro endpoint o escribe a soporte. |
Errores #
Todos los errores usan application/problem+json con trace_id (ver Errores).
El código del motor viene en detail.
| HTTP | Código (en detail) | Cuándo |
|---|---|---|
401 | — | Falta X-API-Key. |
403 | — | La llave no tiene el scope, está vencida o revocada, o la IP no está permitida. |
404 | WEBHOOK_ENDPOINT_NOT_FOUND | El id no existe o es de otra organización. |
409 | WEBHOOK_ENDPOINT_DISABLED | Intentaste reactivar un endpoint deshabilitado por Compense. |
409 | — | Misma Idempotency-Key con la petición original aún en curso. |
422 | WEBHOOK_INVALID_URL | URL que no es HTTPS, host privado o interno, o con credenciales. |
422 | WEBHOOK_INVALID_EVENTS | events vacío o con claves fuera del catálogo. |
422 | WEBHOOK_INVALID_FIELD | Campo no modificable (p. ej. secret) o active no booleano. |
422 | WEBHOOK_ENDPOINT_LIMIT | Tu organización ya tiene 20 endpoints. |
429 | — | Límite por minuto o cuota mensual de la llave. |
Desde la consola: la CLI compense #
Los SDK de Python y JavaScript traen la CLI compense (en Python se instala con el paquete;
en Node, npx compense). Lee la llave de $COMPENSE_API_KEY y apunta al sandbox por defecto. Para
este flujo tiene tres órdenes:
export COMPENSE_API_KEY="cmp_..."
compense events --category wallets # catálogo (filtrado)
compense endpoint-create \
--url https://erp.nova.example/compense/webhooks \
--event credits.decision \
--event wallets.topup.completed \
--label "ERP Nova" # imprime el endpoint con su secreto, UNA vez
compense endpoints # tus endpoints, sin secretoLa CLI no tiene órdenes para editar, pausar, eliminar ni rotar: hazlo con el SDK o con cURL. Todas las órdenes en La CLI compense.