Volver al blog
Desarrolladores

Reenviar un webhook de Waspy que falló: deliveries y replay en la API v2

Listá las entregas fallidas con GET /api/v2/webhooks/:id/deliveries y reenvialas con POST /replay/:deliveryId. Si la fila todavía tiene el data, el replay lo repite. Sé idempotente sobre data.id.

· 12 min
Reenviar un webhook de Waspy que falló: deliveries y replay en la API v2

Respuesta corta: en un pico, las entregas que tu URL no pudo tomar se listan con GET https://api.waspytech.com/api/v2/webhooks/:id/deliveries?status=failed. Para dispararlas de nuevo, POST /api/v2/webhooks/:id/replay/:deliveryId. Si la fila todavía guarda el data, el replay repite ese objeto y el mismo event. Tu handler tiene que ser idempotente sobre data.id.

La base es https://api.waspytech.com/api/v2. La referencia está en webhooks, el sobre en introducción y el catálogo en eventos. El alta y la firma están en webhooks en Node y Python y en verificar la firma. El CRUD está en gestionar webhooks. La clave pública entra en Scale y Enterprise: la API y Planes.

Lo que dice la doc y lo que hace el worker

TemaPágina de docs, hoyEl servidor
Body del replay“No se guarda verbatim, solo el hash. El replay es sintético.”Desde la migración 0046 la fila guarda payload (el data). El replay lo reencola si no es null
Qué ves en el GETpayloadHashIgual: el JSON público no trae payload. El hash es SHA-256 del sobre crudo
Si payload es nullEl caso que la doc describe como únicoStub { replayOf, replayedAt, originalEvent } como data
Quinto hueco de reintentoLa descripción de la ruta lista 12 horasCon 5 intentos el worker agenda 1 min, 5 min, 30 min y 2 horas. El slot de 12 horas no se usa
La nota del 28 de octubre repite el texto viejo de la doc (replay sintético y una espera de 12 horas). Esta nota sigue al worker de apps/worker/src/webhook-deliveries-worker.ts y a replayWebhookDelivery. El comentario de la columna habla de vaciar payload a los 30 días. En este repo no hay un cron que lo haga. Mientras la columna tenga valor, el replay manda ese data.

Listar las que fallaron

Scope webhooks:read. status es success, failed o all (default all). limit arranca en 50 y llega a 100. El orden es el más nuevo primero.

curl -sS "https://api.waspytech.com/api/v2/webhooks/SUB_ID/deliveries?status=failed&limit=50" \
  -H "Authorization: Bearer $WASPY_API_KEY"

Cada ítem de data trae id, subscriptionId, event, attempt, success, statusCode, errorMessage, responseBodyTruncated, payloadHash, deliveredAt y createdAt. El id es el deliveryId del replay. meta.cursor y meta.hasMore paginan. Copiá el cursor tal cual: si lo fabricás, la API responde 400 INVALID_CURSOR. El formato está en paginación.

Un id de suscripción que no es de tu tenant responde 404 WEBHOOK_NOT_FOUND. Un deliveryId que no es UUID no llega al handler: 400 VALIDATION_ERROR.

Reenviar una entrega

Scope webhooks:write. deliveryId es un UUID de esa suscripción.

curl -sS -X POST "https://api.waspytech.com/api/v2/webhooks/SUB_ID/replay/DELIVERY_ID" \
  -H "Authorization: Bearer $WASPY_API_KEY"

El 200 es data.enqueued: true, data.event (el de la fila) y data.subscriptionId. No trae samplePayload: eso lo devuelve solo el test. Un delivery de otra suscripción o de otro tenant es 404 DELIVERY_NOT_FOUND.

El 200 quiere decir “encolado”, no “tu URL ya respondió 2xx”. El worker arma un sobre nuevo: event, webhookId, deliveredAt de ahora, attempt: 1 y data igual al payload guardado. La firma HMAC usa un t fresco. X-Waspy-First-Attempt-At es el de esta cadena de replay, no el del intento original. El algoritmo está en seguridad.

Si payload es null, data queda así:

{ "replayOf": "uuid-de-la-fila", "replayedAt": "2026-11-03T18:00:00.000Z", "originalEvent": "message.received" }

Ese stub no tiene data.id. No lo trates como un mensaje nuevo del cliente. Sirve para ver que la URL, la firma y el transporte responden.

Idempotencia en el pico

Un replay de una fila reciente repite el mismo data.id (el UUID del mensaje en Waspy, en message.received y en message.status_changed). Si el primer POST sí procesó el pedido y después tu proceso se cayó al responder, el segundo POST no puede crear otro pedido. La clave es data.id. Cómo separar eso de Idempotency-Key y de clientRef está en idempotencia y scopes.

Los cuatro event que acepta el alta son message.received, message.status_changed, conversation.created y contact.created. El replay conserva el que tenía la fila. No lo convierte todo en message.received.

Test, replay y el contador de fallas

Entrega realPOST .../testPOST .../replay/:deliveryId
Qué mandaEl data del eventomessage.received con datos dummy. La respuesta incluye samplePayloadEl data guardado, o el stub
Aparece en deliveriesSíSíSí
Mueve lastDeliveryAt y consecutiveFailuresSíNoNo
Si falla, ¿reintenta?Sí, hasta 5Sí, hasta 5Sí, hasta 5
Suma a las 10 que apaganSíNoNo
El test y el replay se encolan con isTest: true. Por eso un replay durante el CyberMonday no “arregla” el contador ni lo empeora: las 10 fallas seguidas salen del tráfico real. Para ver si el replay pegó, volvé a leer GET .../deliveries?status=all&limit=5. No mires lastDeliveryAt.

Si la suscripción está enabled: false, el POST de replay igual responde 200 encolado, y el worker no entrega y no escribe otra fila. Antes de reenviar, reactivala:

curl -sS -X PATCH "https://api.waspytech.com/api/v2/webhooks/SUB_ID" \
  -H "Authorization: Bearer $WASPY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true }'

Ese PATCH pone consecutiveFailures en 0 y limpia disabledAt y disabledReason.

Qué cuenta como éxito, y cuándo se apaga

Tu URLResultado
2xxEntrega ok
Cualquier 3xxEntrega ok. Se guarda ese status. No hay un segundo POST a Location
4xx o 5xxFallo
Nada en 10 segundosFallo
En una entrega real, la falla número 10 seguida deja enabled: false. El motivo queda en disabledReason. Los huecos entre los cinco intentos son 1 minuto, 5 minutos, 30 minutos y 2 horas. Respondé 2xx enseguida y procesá después: el body de tu respuesta no cambia el resultado.

La API REST está en Scale (US$150 por mes) y Enterprise. Diez días sin tarjeta, en el registro. Los planes están en Planes.

Tu equipo atendiendo por WhatsApp en minutos

CRM facil pero potente. En 10 minutos tenes a tu equipo trabajando — no necesitas hablar con nadie.