Cómo gestionar webhooks de la API de Waspy (alta, firma, reintentos)
CRUD en /api/v2/webhooks: crear, listar, editar, borrar, probar, rotar el secret y reenviar. Un 2xx o un 3xx es entrega. El log pagina con cursor opaco. Scale y Enterprise.
Respuesta corta: das de alta un webhook con POST https://api.waspytech.com/api/v2/webhooks, scope webhooks:write. Guardá el secret de esa respuesta: no vuelve a salir. Tu URL tiene que responder 2xx o 3xx en menos de 10 segundos. La firma va en X-Waspy-Signature. La API pública está en Scale y Enterprise.
La referencia está en webhooks, el modelo de entrega en introducción y la firma en seguridad. Cómo verificar el HMAC en Node y Python, sin repetir el algoritmo acá, está en verificar la firma.
Qué podés hacer
| Método | Ruta | Scope | Para qué |
|---|---|---|---|
| GET | /api/v2/webhooks | webhooks:read | Listar las del tenant |
| POST | /api/v2/webhooks | webhooks:write | Crear. El secret sale una vez |
| GET | /api/v2/webhooks/:id | webhooks:read | Una suscripción, sin secret |
| PATCH | /api/v2/webhooks/:id | webhooks:write | url, events, description, enabled, headers |
| DELETE | /api/v2/webhooks/:id | webhooks:write | Borrar |
| POST | /api/v2/webhooks/:id/test | webhooks:write | Evento sintético message.received |
| GET | /api/v2/webhooks/:id/deliveries | webhooks:read | Log de intentos, cursor opaco |
| POST | /api/v2/webhooks/:id/rotate-secret | webhooks:write | Secret nuevo, el viejo muere ya |
| POST | /api/v2/webhooks/:id/replay/:deliveryId | webhooks:write | Reencolar una entrega del log |
Crear la suscripción
url tiene que ser HTTPS. La query string se conserva en cada entrega. events es un array con al menos uno de estos cuatro, los únicos que acepta el schema:
| Evento | Cuándo |
|---|---|
message.received | Entra un mensaje |
message.status_changed | Un saliente pasa a sent, delivered, read o failed |
conversation.created | Se crea una conversación |
contact.created | Se crea un contacto |
curl -sS -X POST https://api.waspytech.com/api/v2/webhooks \
-H "Authorization: Bearer $WASPY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tu-bot.example.com/webhook/waspy",
"events": ["message.received", "message.status_changed"],
"description": "CRM de pedidos"
}'
La respuesta 201 trae data.secret (empieza con whsec_ en los ejemplos de la doc). Copialo a WASPY_WEBHOOK_SECRET antes de cerrar la terminal. GET de después no lo incluye.
headers es opcional: hasta 10 headers extra, nombre de hasta 256 caracteres y valor de hasta 1024, sin saltos de línea. Van en el primer intento, en los reintentos y en el test. No podés pisar Content-Type, Content-Length, Host, X-Waspy-Signature, X-Waspy-Webhook-Id, X-Waspy-Webhook-Event, X-Waspy-Webhook-Version, X-Waspy-Webhook-Attempt ni X-Waspy-First-Attempt-At. En los GET el valor vuelve enmascarado (**). En un PATCH, headers reemplaza el conjunto entero: null o {} los borra; si omitís el campo, quedan como estaban.
Qué cuenta como entrega
| Tu URL responde | Qué hace Waspy |
|---|---|
| 2xx | Entrega ok |
| Cualquier 3xx | Entrega ok. Guarda ese status (un 302 queda como 302). No reintenta |
| 4xx o 5xx | Fallo. Entra en los reintentos |
| Nada en 10 segundos | Fallo |
doPost. Eso es éxito. Si lo contáramos como error, el mismo evento se procesaría otra vez. Waspy no repite el POST contra Location. Si hay Location, puede hacer un GET solo para anotar qué devolvió el redirect, después de validar esa URL. Si ese GET falla, la entrega sigue contando como exitosa.
La misma regla vale para el test y para el replay.
Reintentos y apagado
Si falla, los intentos siguientes esperan así:
| Intento | Espera antes |
|---|---|
| 2 | 1 minuto |
| 3 | 5 minutos |
| 4 | 30 minutos |
| 5 | 2 horas |
| Último | 12 horas |
enabled: false. PATCH con { "enabled": true } la reactiva y resetea el contador.
Respondé 200 enseguida y procesá el evento después. El body no cambia el resultado. Si la firma no cierra, un 401 deja el error asentado. El algoritmo (HMAC-SHA256 de t + . + body crudo, ventana de 5 minutos sobre t, edad del payload en X-Waspy-First-Attempt-At) está en verificar la firma y en la doc de seguridad.
Probar, leer el log y reenviar
POST /webhooks/:id/test encola un message.received de prueba. La respuesta trae samplePayload con la forma del sobre. Esas pruebas sí aparecen en GET /webhooks/:id/deliveries, y no actualizan lastDeliveryAt, lastDeliveryStatus ni consecutiveFailures. Para ver si pegó, consultá el log justo después.
curl -sS "https://api.waspytech.com/api/v2/webhooks/SUB_UUID/deliveries?status=failed&limit=20" \
-H "Authorization: Bearer $WASPY_API_KEY"
meta.cursor es opaco y meta.hasMore dice si hay otra página. No armes el cursor: un valor fabricado responde 400 INVALID_CURSOR. Cómo está armada la paginación de toda la API está en page y cursor.
El id de cada fila es el deliveryId de POST /webhooks/:id/replay/:deliveryId. El replay no reenvía el body original guardado: la tabla guarda el hash, no el payload verbatim. Vuelve a disparar el mismo event con datos sintéticos. Tu handler tiene que ser idempotente sobre data.id. El catálogo de cada data está en eventos.
message.status_changed es el lugar donde vuelve el clientRef que mandaste al enviar. Ese recorrido está en reconciliar estados.
Quién tiene la API
La API REST con webhooks está en los planes Scale (US$150 por mes) y Enterprise. Starter y Growth atienden desde el panel, sin esta clave. Diez días de prueba, sin tarjeta, en el registro. El detalle de cada plan está en Planes.
