Webhooks
Avisos firmados de lo que pasa con tus reservas, con reintentos automáticos, log de entregas y ejemplos para verificar la firma.
Actualizado:
Eventos
Hoy se pueden suscribir estos eventos de reservas:
| Evento | Cuándo |
|---|---|
booking.created | Se creó una pre-reserva o reserva |
booking.confirmed | Se confirmó una reserva |
booking.cancelled | Cancelada o vencida |
booking.rescheduled | Se movió de horario (el payload trae el id viejo y el nuevo) |
booking.no_show | Marcada como ausencia |
booking.completed | Atención completada |
El contrato reserva además payment.paid, message.received y conversation.handoff, pero todavía no se pueden suscribir ni se emiten (el cobro de la seña es Próximamente). Un endpoint que los pida recibe 422.
Creá endpoints desde Configuración → Webhooks en el panel o con POST /v1/webhook-endpoints (url y events). El secreto de firma (whsec_…) se muestra una sola vez.
curl -X POST https://api.fiuit.com/v1/webhook-endpoints \
-H "Authorization: Bearer $WAGEND_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/wagend", "events": ["booking.confirmed", "booking.cancelled"] }'
Payload
{
"id": "evt_3f6c1d9e0b7a4c2f8e5d1a9b7c3e6f20",
"type": "booking.confirmed",
"created_at": "2026-10-14T15:21:07-03:00",
"workspace_id": "b3a6c8e2-5f1d-4c7a-9e0b-2d8f4a1c6e53",
"data": { "booking": { "id": "6d1f0c4a-7b2e-4a58-9c3d-0e5f8a2b1c47", "status": "confirmed", "start_at": "2026-10-15T12:45:00+00:00" } }
}
La entrega es al menos una vez: usá el id para ignorar duplicados.
Verificar la firma
Cada request trae el header:
Wagend-Signature: t=1791040867,v1=5c2b9f...e81
v1 es HMAC-SHA256(secreto, t + "." + body_crudo) en hexadecimal. Rechazá requests de más de 5 minutos y calculá la firma sobre el cuerpo crudo, sin volver a serializar el JSON.
import crypto from 'node:crypto'
export function verifyWagend(rawBody: string, header: string, secret: string) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=') as [string, string]))
const age = Math.abs(Date.now() / 1000 - Number(parts.t))
if (!parts.t || !parts.v1 || age > 300) return false
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}
import hashlib, hmac, time
def verify_wagend(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts.get("t", "0"))) > 300:
return False
signed = f"{parts['t']}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
Reintentos
Respondé con cualquier 2xx en menos de 10 segundos. Si no, reintentamos a los 1 min, 5 min, 30 min, 2 h y 12 h. Cada intento se ve en el log de entregas, donde también podés reenviar a mano.
Seguridad y límites
- La URL debe ser
httpsy resolver a una dirección pública: rechazamoslocalhost, redes privadas, link-local y direcciones de metadatos de nube (al registrar y en cada entrega). No seguimos redirecciones (un3xxcuenta como fallo). - Headers extra:
Wagend-Event(tipo) yWagend-Delivery(id de la entrega). Cada intento se firma con un timestamp nuevo. - Hasta 10 endpoints por workspace. Tras 10 fallos consecutivos el endpoint se desactiva (reactivalo con
PATCH /v1/webhook-endpoints/{id}y{"active": true}). POST /v1/webhook-endpoints/{id}/pingenvía un evento de pruebawebhook.ping;POST /v1/webhook-endpoints/{id}/rotate-secretgenera un secreto nuevo (se muestra una sola vez; el anterior deja de valer al instante).- Con API key usá el scope
webhooks:manage. El log está enGET /v1/webhook-endpoints/{id}/deliveriesy el reenvío enPOST …/deliveries/{delivery_id}/resend.