04 — Webhooks

Los webhooks son la forma recomendada de enterarte de un cobro en cuanto ocurre, sin hacer polling a GET /v1/transactions. Requiere webhooks:read para consultar y webhooks:write para crear, modificar, probar o rotar.

POST /v1/webhooks

curl -X POST https://api.yallego.app/v1/webhooks \
  -H "Authorization: Bearer yk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://mi-sistema.pe/hooks/yallego",
    "subscribed_events": ["transaction.created", "device.offline"],
    "description": "Integración con sistema de ventas"
  }'
{
  "id": "...",
  "url": "https://mi-sistema.pe/hooks/yallego",
  "secret": "whsec_3f6a8c2e1b5d7f9a3c6e9f8c2a1e4b7d",
  "subscribed_events": ["transaction.created", "device.offline"],
  "is_enabled": true,
  "created_at": "2026-05-14T18:50:00Z"
}

Requisitos de la URL:

  • Debe ser https:// — se rechaza http:// con 400 VALIDATION_ERROR y el campo url en details.fields (ver errores de validación).
  • Puede usar cualquier puerto (https://mi-sistema.pe:8443/hooks): no hay una lista de puertos permitidos. Lo que se valida son las direcciones.
  • No puede resolver a una red privada, de enlace local, o loopback (SSRF): todas las direcciones del dominio tienen que ser públicas. La validación ocurre al crear el endpoint y antes de cada intento de entrega. Si después de creado tu dominio empieza a resolver a una IP privada, la entrega se abandona en ese intento, sin reintentos; si deja de resolver (un corte de DNS), ese intento cuenta como fallido y la entrega sigue el calendario de reintentos (ver 08 — Política de reintentos).
  • Las entregas no siguen redirecciones: un 3xx cuenta como fallo. Registra la URL final.
  • El número de endpoints está limitado por plan (webhooks en los límites del plan); superarlo responde 422 PLAN_LIMIT_EXCEEDED. Es un cupo aparte del de claves de API: crear claves no consume webhooks.

secret se muestra una única vez en esta respuesta. Guárdalo — lo necesitas para verificar la firma de cada evento entrante.

subscribed_events acepta los eventos del catálogo. Un evento fuera del catálogo responde 400 VALIDATION_ERROR; eso incluye notification.unmatched, que se retiró porque nunca se emitió (ver «Eventos retirados» en el catálogo de eventos).

Cada evento trae en el sobre tenant_id (el comercio) y livemode, así que puedes registrar la misma URL en varios comercios y enrutar por tenant_id (ver «Un endpoint para varios comercios» en el catálogo de eventos).

Envía Idempotency-Key al registrar: si la respuesta se pierde y repites con la misma clave, recibes el mismo endpoint (y el mismo secret) en vez de crear un duplicado que consuma cupo del plan. La prueba, la rotación del secreto y el reintento manual también la aceptan. Ver 10 — Idempotencia.

Gestión del endpoint

MétodoRutaDescripción
GET/v1/webhooksLista todos los endpoints del tenant
GET/v1/webhooks/{id}Detalle de un endpoint
PATCH/v1/webhooks/{id}Actualiza subscribed_events, description o is_enabled
DELETE/v1/webhooks/{id}Elimina el endpoint permanentemente
POST/v1/webhooks/{id}/testEnvía un evento transaction.created de prueba con datos ficticios

El evento de prueba viaja con el mismo sobre y la misma firma que uno real, con el tenant_id y el livemode del comercio; solo data es ficticio. Cada llamada crea un evento nuevo (otro id) y una entrega nueva, y responde { "delivery_id": "..." } para seguirla en el historial.

PATCH con subscribed_events reemplaza la lista completa. Si la lista incluye notification.unmatched, se descarta en vez de responder 400.

GET /v1/webhooks y GET /v1/webhooks/{id} incluyen además un resumen de las entregas creadas en las últimas 7 × 24 horas:

{
  "id": "...",
  "url": "https://mi-sistema.pe/hooks/yallego",
  "is_enabled": true,
  "consecutive_failures": 0,
  "deliveries_last_7_days": { "total": 128, "delivered": 125, "failed": 1 }
}

delivered cuenta las entregas DELIVERED y failed, las ABANDONED (ver reintentos). El resto de total está esperando un intento (PENDING). Las respuestas de crear, actualizar y rotar no lo incluyen.

# Deshabilitar temporalmente sin borrar la configuración
curl -X PATCH https://api.yallego.app/v1/webhooks/{id} \
  -H "Authorization: Bearer yk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"is_enabled": false}'

POST /v1/webhooks/{id}/rotate-secret

Genera un secreto nuevo sin cambiar el id ni la URL del endpoint. Durante las 24 horas siguientes, cada entrega se firma con AMBOS secretos:

X-Yallego-Signature: sha256=<firmado con el secreto NUEVO>
X-Yallego-Signature-Previous: sha256=<firmado con el secreto ANTERIOR>

Esto te da una ventana para actualizar tu verificación en producción sin perder entregas en el camino — verifica contra X-Yallego-Signature primero y, si no tienes el secreto nuevo desplegado aún, cae a X-Yallego-Signature-Previous. Pasadas las 24 horas, solo se envía la firma nueva. Esta cabecera adicional es una extensión propia de Yallegó (no forma parte del contrato base de firma, ver guía 07).

curl -X POST https://api.yallego.app/v1/webhooks/{id}/rotate-secret \
  -H "Authorization: Bearer yk_live_..."
# Respuesta: mismo shape que POST /v1/webhooks, con el nuevo `secret`

Historial de entregas

curl "https://api.yallego.app/v1/webhooks/{id}/deliveries?status=ABANDONED&limit=20" \
  -H "Authorization: Bearer yk_live_..."
{
  "data": [
    {
      "id": "...",
      "event_id": "3f6a8c2e-...",
      "event_type": "transaction.created",
      "status": "ABANDONED",
      "attempts": 8,
      "max_attempts": 8,
      "next_attempt_at": null,
      "last_attempt_at": "2026-05-14T19:32:07Z",
      "last_status_code": 503,
      "last_error": "HTTP 503",
      "delivered_at": null,
      "created_at": "2026-05-14T18:32:09Z"
    }
  ],
  "pagination": { "has_more": true, "next_cursor": "MjAyNi0wNS0xNFQx...", "limit": 20 }
}

Las entregas vienen de la más reciente a la más antigua. Para la página siguiente, repite la consulta con cursor=<next_cursor> y los mismos filtros, igual que en el listado de transacciones; cuando has_more es false, next_cursor es null. limit va de 1 a 100 (20 por omisión). Un cursor que no salió de esta ruta responde 400 VALIDATION_ERROR.

id es el mismo valor que llega en la cabecera X-Yallego-Delivery-Id de cada intento de esa entrega.

Una entrega pasa por tres estados:

EstadoSignificado
PENDINGEsperando su intento: el primero o el siguiente del calendario. attempts, last_status_code y last_error dicen cómo fue el anterior y next_attempt_at, cuándo es el próximo
DELIVEREDTu endpoint respondió 2xx
ABANDONEDNo se reintenta más: agotó los 8 intentos, tu endpoint respondió 410, el dominio resolvió a una red interna, o el endpoint estaba deshabilitado (ver 08)

IN_PROGRESS y FAILED existen en el enum (el filtro status los acepta) pero hoy no se asignan: un intento fallido con reintentos pendientes vuelve a PENDING, y filtrar por ellos devuelve una lista vacía. Para ver las entregas que fallaron definitivamente, filtra por ABANDONED.

POST /v1/webhooks/{id}/deliveries/{deliveryId}/retry

Reintenta manualmente una entrega ABANDONED (o adelanta una PENDING), por ejemplo después de arreglar un bug en tu endpoint. Hace un intento inmediato de la misma entrega: mismo id, mismo X-Yallego-Delivery-Id y mismo cuerpo. En una PENDING, ese intento reemplaza al que estaba agendado: no se suma uno más.

curl -X POST https://api.yallego.app/v1/webhooks/{id}/deliveries/{deliveryId}/retry \
  -H "Authorization: Bearer yk_live_..."

Responde { "delivery_id": "..." }. Reintentar una DELIVERED responde 409 CONFLICT. Si el endpoint está deshabilitado, reactívalo antes (PATCH con {"is_enabled": true}): con el endpoint deshabilitado la entrega se abandona sin enviarse. Qué pasa si el intento falla está en 08 — Política de reintentos.

En todas las rutas de webhooks, un {id} o {deliveryId} que no es un UUID responde 404 NOT_FOUND, igual que uno que no existe.