Antes de salir a producción quieres ver tu receptor aceptando una entrega firmada de verdad, rechazando una alterada y respondiendo a tiempo. Esta página cubre las cuatro herramientas para lograrlo: el evento de prueba del API, la CLI compense (trigger y listen), las pruebas unitarias sin red y el sandbox de webhooks.

Evento de prueba por API #

POST/v1/gateway/webhooks/endpoints/{endpoint_id}/test/ hace que Compense envíe a uno de tus endpoints una entrega firmada exactamente como una real:

  • sin cuerpo, el tipo es test.ping;
  • con event_type, se envía el payload_example de ese evento del catálogo (por ejemplo credits.decision), para que pruebes el manejador de ese evento.

La prueba se distingue de una entrega real de dos formas: el payload trae "test": true y el event_id (cabecera X-Webhook-Id) empieza por test-. Tu receptor debe responderla con 2xx y no procesarla como real (ver Ignora los eventos de prueba). El endpoint debe estar active; si no, la respuesta es 409 WEBHOOK_ENDPOINT_INACTIVE.

import os
from compense_gateway import CompenseClient

client = CompenseClient(api_key=os.environ["COMPENSE_API_KEY"], environment="sandbox")

tx = client.webhooks.endpoints.send_test(
    "3f1a2b3c-0000-4a1b-9c2d-000000000001",
    event_type="credits.decision",           # omítelo para un test.ping
)
print(tx.data["delivery_id"], tx.data["event_id"], tx.data["status"])

# Sigue el resultado de la entrega:
entrega = client.webhooks.deliveries.get(tx.data["delivery_id"])
print(entrega["status"], entrega.get("response"))
JSON
{
  "status": "success",
  "transaction_id": "a1b2c3d4-0000-4a1b-9c2d-000000000040",
  "module": "webhooks",
  "data": {
    "delivery_id": "8b7c6d5e-0000-4a1b-9c2d-000000000401",
    "event_type": "credits.decision",
    "event_id": "test-…",
    "status": "pending"
  }
}

El cuerpo que recibe tu endpoint lleva el sobre normal más la marca de prueba:

JSON
{
  "event": "credits.decision",
  "version": "1",
  "tenant_id": "nova",
  "occurred_at": "2026-11-02T14:00:00Z",
  "resource": { "…": "payload_example del catálogo" },
  "test": true
}

client.sandbox.trigger_webhook quedó deprecado (Python)

La versión 1.0 del SDK de Python tenía client.sandbox.trigger_webhook(...), que llamaba una ruta que el gateway no publica. Hoy es un atajo sobre webhooks.endpoints.send_test y emite un DeprecationWarning: usa send_test directamente.

Desde la consola: compense trigger #

La CLI compense (incluida en los SDK de Python y JavaScript) envuelve la prueba en una orden. Sin --endpoint, elige un endpoint activo suscrito a ese evento; si no hay ninguno, el primero activo:

Shell
export COMPENSE_API_KEY="cmp_..."

compense trigger credits.decision                       # elige el endpoint por ti
compense trigger wallets.topup.completed --endpoint 3f1a2b3c-0000-4a1b-9c2d-000000000001
compense deliveries --status failed                     # ¿alguna prueba falló?

trigger imprime en stderr a qué endpoint envió la prueba y su delivery_id, y en stdout el data de la respuesta como JSON. Si no tienes ningún endpoint activo, termina con código 2 y te pide crear uno.

Recibir entregas reales en localhost: compense listen #

Un endpoint debe ser una URL HTTPS pública: Compense no puede llamar a http://localhost:8000. Para desarrollar en tu máquina con entregas reales:

  1. Abre un túnel HTTPS hacia tu máquina con la herramienta que uses (ngrok, cloudflared o similar) apuntando al puerto del listener (8787 por defecto).
  2. Registra la URL pública del túnel como endpoint (con compense endpoint-create o el SDK) y guarda el secreto en tu receptor local.
  3. Corre compense listen: recibe cada entrega en 127.0.0.1:8787 y la reenvía tal cual (cuerpo crudo y cabeceras de firma intactos) a tu receptor local, así que tu verificación HMAC funciona igual que en producción. Imprime las cabeceras de cada entrega y le devuelve a Compense el código que respondió tu receptor, para que los reintentos reflejen la realidad.
Shell
compense listen --forward-to http://localhost:8000/compense/webhooks --port 8787
# (con el SDK de JavaScript: npx compense listen --forward-to … --port 8787)
Texto
→ POST /  event=credits.decision
    X-Webhook-Signature: sha256=…
    X-Webhook-Timestamp: 1793628000
    X-Webhook-Event: credits.decision
    X-Webhook-Id: test-…
    ↳ forwarded to http://localhost:8000/compense/webhooks → 200
sequenceDiagram
    participant C as Compense (ambiente dev)
    participant T as Túnel HTTPS público
    participant L as compense listen :8787
    participant A as Tu receptor :8000
    C->>T: POST firmado
    T->>L: POST (mismos bytes)
    L->>A: POST (cuerpo crudo + cabeceras de firma)
    A-->>L: 200 / 401 / 500
    L-->>T: el mismo código
    T-->>C: el mismo código

listen necesita entregas reales: no funciona con el sandbox

El sandbox no hace POST HTTP a tu URL (las pruebas y los replays responden "simulated": true), así que con una llave de sandbox listen nunca recibirá nada. Para recibir entregas firmadas de verdad usa una llave de producción en el ambiente dev (--environment dev, ver Entornos). listen no usa la llave: solo reenvía; la llave la usan endpoint-create y trigger.

Prueba tu receptor sin red #

Para pruebas unitarias o de integración no necesitas a Compense: firma tú mismo un cuerpo con un secreto de prueba y pásalo por la verificación. Los SDK exponen la misma función que usa el despachador para calcular la firma.

Firmar y verificar una entrega de prueba
import json
import time

from compense_gateway.webhooks import expected_signature, verify_webhook

SECRETO = "whsec_prueba_local"
cuerpo = json.dumps(
    {"event": "credits.decision", "version": "1", "tenant_id": "horizonte",
     "occurred_at": "2026-10-27T15:20:00Z", "resource": {"decision": "approved"}},
    separators=(",", ":"),                   # JSON compacto, como lo envía Compense
).encode("utf-8")
ahora = int(time.time())
cabeceras = {
    "X-Webhook-Signature": "sha256=" + expected_signature(SECRETO, cuerpo),
    "X-Webhook-Event": "credits.decision",
    "X-Webhook-Id": "evt-prueba-0001",
    "X-Webhook-Timestamp": str(ahora),
}

event = verify_webhook(cuerpo, cabeceras, secret=SECRETO, now=ahora)
assert event.resource["decision"] == "approved"
# Con Flask: app.test_client().post("/compense/webhooks", data=cuerpo, headers=cabeceras)

Casos que vale la pena cubrir en tu suite:

  • Entrega válida → 200 y el efecto aplicado una vez.
  • La misma entrega dos veces (mismo X-Webhook-Id) → 200 y el efecto aplicado una vez.
  • Un carácter del cuerpo cambiado, o firmada con otro secreto → 401.
  • Timestamp de hace 10 minutos → 401 (ventana de 300 s).
  • "test": true → 200 sin efecto.
  • Evento que no manejas → 200 sin efecto.
  • Firmada con el secreto anterior durante una rotación → 200.

Diferencias del sandbox #

Con una llave de sandbox, todo el módulo de webhooks responde de forma determinista y sin tocar el motor real. Sirve para escribir y probar tu código de administración (crear, listar, editar, rotar, consultar entregas, reintentar), no para recibir entregas.

AspectoSandboxProducción
Envío HTTP a tu URLNo hay: la prueba y el replay responden "simulated": truePOST real firmado
Catálogo9 eventos de muestra, con "sample": true y un payload_example genéricoCompleto (52 eventos)
Validación de eventsSolo de formato (recurso.evento, minúsculas y _)Contra el catálogo completo
Id del endpointwhe_sbx_<hash>: la misma URL da siempre el mismo id. Cualquier whe_sbx_… existe; cualquier otro id responde 404UUID
Secretowhsec_sbx_<hash>, fijo por endpoint (no es real: sirve para practicar la verificación). Rotar devuelve otro secreto fijoAleatorio, mostrado una vez
Lista de endpointsSiempre un endpoint de muestraLos tuyos
Evento de pruebadelivery_id whd_sbx_test_… y event_id test-sbx-…Entrega real
Webhooks de otros módulosNo se emiten (una PQRS radicada en sandbox no genera pqrs.created)Se emiten

Y tres entregas fijas por organización, con fechas fijas para que los filtros sean deterministas:

IdEstadoEventoFechaReplay
whd_sbx_ok_…successcredits.decision2026-10-01409 WEBHOOK_DELIVERY_NOT_REPLAYABLE
whd_sbx_fail_…failed (HTTP 500)wallets.cashin.confirmed2026-10-02201, crea whd_sbx_retry_…
whd_sbx_aban_…abandoned (HTTP 503)credits.disbursed2026-10-03201, crea whd_sbx_retry_…

Con ellas puedes probar tu script de reprocesar las fallidas: en el sandbox recolecta exactamente dos entregas (fail y aban) y la de éxito responde 409 si intentas reintentarla. El resto de valores de prueba del sandbox está en Datos de prueba.