# Webhooks Los webhooks envían eventos a tu sistema en el momento en que se producen, así que no necesitas consultar la API periódicamente. ## Eventos | Evento | Cuándo | |---|---| | `booking.created` | Se ha creado una reserva (panel, importación desde un canal o API) | | `booking.updated` | Han cambiado las fechas, los huéspedes, las horas de check-in/check-out o los datos de contacto | | `booking.cancelled` | Se ha cancelado una reserva | | `checkin.completed` | El huésped ha completado (o actualizado) el check-in online | | `payment.completed` | Tasa turística pagada o fianza retenida/pagada (también cuando el personal la marca como pagada) | | `booking.unlocked` | Todos los pasos están completados: la guía de la estancia está desbloqueada | | `extras.ordered` | El huésped ha pagado extras, incluido un check-in anticipado o un check-out tardío | | `message.sent` | Se ha enviado un mensaje al huésped a través del canal de la reserva (Airbnb, Booking.com … vía Hostaway o Channex) | | `feedback.received` | El huésped ha valorado la estancia (1–5) en la página de opiniones tras una solicitud de reseña | | `ping` | Evento de prueba del botón **Test** | ### Qué contiene `data` | Evento | Campos de `data` | |---|---| | `booking.created`, `booking.updated`, `booking.cancelled` | `bookingId`, `ref`, `source` (importaciones desde canales); `reason: "stay-times"` cuando un check-in anticipado / check-out tardío comprado ha cambiado las horas | | `checkin.completed` | `bookingId`, `ref` | | `payment.completed` | `bookingId`, `leg` (`eco` o `deposit`), `status` (`paid` o `held`), `manual: true` cuando el personal lo marcó como pagado | | `booking.unlocked` | `bookingId` | | `extras.ordered` | `bookingId`, `leg: "extras"`, `status: "paid"` | | `message.sent` | `bookingId`, `ref`, `via` (p. ej. `Hostaway`) | | `feedback.received` | `bookingId`, `ref`, `rating` (1–5) | La versión legible por máquina de cada evento (cabeceras, cuerpo y `data`) está en la sección `webhooks` del [documento OpenAPI](/api/v1/openapi.json) y en la [referencia de la API](/api/docs#webhooks). ## Añadir un webhook **Settings → Webhooks**: introduce una URL `https://`, marca los eventos que quieres recibir (ninguno = todos) y haz clic en **Add webhook**. El **secreto de firma** se muestra una sola vez: guárdalo junto a tu receptor. ![Settings → Webhooks](/site/img/docs/d-settings-webhooks.webp "Settings → Webhooks") ## La solicitud `POST` con un cuerpo JSON: ``` { "id": "evt_…", "event": "payment.completed", "createdAt": "2026-11-18T14:03:11.000Z", "tenant": "t_…", "data": { "bookingId": "b_…", "leg": "eco", "status": "paid" } } ``` Cabeceras: | Cabecera | Contenido | |---|---| | `X-GP-Event` | el nombre del evento | | `X-GP-Delivery` | ID único de la entrega | | `X-GP-Timestamp` | Tiempo Unix en segundos | | `X-GP-Signature` | `v1=` + HMAC-SHA256 en hexadecimal de `.` con tu secreto de firma | ## Verificar la firma (Node.js) ``` const crypto = require('crypto'); function verify(rawBody, headers, secret) { const ts = headers['x-gp-timestamp']; const expected = 'v1=' + crypto.createHmac('sha256', secret).update(ts + '.' + rawBody).digest('hex'); const given = String(headers['x-gp-signature'] || ''); const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300; return fresh && given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected)); } ``` Verifica siempre contra el cuerpo **sin procesar** de la solicitud y rechaza las marcas de tiempo antiguas para bloquear ataques de repetición. ## Verificar la firma (C# / .NET) ``` using System.Security.Cryptography; using System.Text; static bool Verify(string rawBody, string ts, string signature, string secret) { if (!long.TryParse(ts, out var t) || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t) > 300) return false; using var h = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); var hex = Convert.ToHexString(h.ComputeHash(Encoding.UTF8.GetBytes(ts + "." + rawBody))).ToLowerInvariant(); return CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes("v1=" + hex), Encoding.ASCII.GetBytes(signature ?? "")); } ``` En .NET Framework, usa `BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant()` en lugar de `Convert.ToHexString`, y compara en tiempo constante con un bucle sencillo. ## Verificar la firma (PHP) ``` $expected = 'v1=' . hash_hmac('sha256', $_SERVER['HTTP_X_GP_TIMESTAMP'] . '.' . file_get_contents('php://input'), $secret); $ok = abs(time() - (int)$_SERVER['HTTP_X_GP_TIMESTAMP']) < 300 && hash_equals($expected, $_SERVER['HTTP_X_GP_SIGNATURE'] ?? ''); ``` ## Entrega y reintentos Responde con cualquier código de estado 2xx en un plazo de 10 segundos. Las entregas fallidas se reintentan a los 30 segundos y a los 5 minutos (tres intentos en total). **Recent deliveries** muestra los últimos 200 intentos con su resultado. - El `id` del cuerpo y la cabecera `X-GP-Delivery` son los mismos en cada reintento: guárdalos e ignora los duplicados. - Los eventos pueden llegar desordenados; usa `createdAt` y consulta la reserva para obtener su estado actual. - No se siguen las redirecciones y solo se llama a direcciones `https://` públicas (no a redes privadas ni locales). - Las solicitudes llegan con `Content-Type: application/json` y `User-Agent: GuestPortal-Webhooks/1`. ## Preguntas frecuentes ### ¿Recibo la reserva completa en el evento? El evento incluye los ID y el cambio. Obtén la reserva completa con `GET /api/v1/bookings/{id}` usando una clave de API; así no viajan datos personales en el cuerpo de los webhooks. ### ¿Pueden los canales enviar webhooks a Guestbud? Sí, es la dirección contraria. Hostaway y Channex pueden avisar a Guestbud al instante de reservas nuevas y modificadas; consulta [Hostaway](/docs/connect-hostaway) y [Channex](/docs/connect-channex). Esas direcciones entrantes son secretas para cada empresa y solo activan una nueva importación de esa reserva. ### ¿Se envían webhooks desde la cuenta de demostración? No.