# Webhook I webhook inviano gli eventi al tuo sistema nel momento in cui avvengono, così non devi interrogare l'API a intervalli regolari. ## Eventi | Evento | Quando | |---|---| | `booking.created` | È stata creata una prenotazione (dashboard, importazione dal canale o API) | | `booking.updated` | Sono cambiati date, ospiti, orari di check-in/check-out o recapiti | | `booking.cancelled` | Una prenotazione è stata cancellata | | `checkin.completed` | L'ospite ha completato (o aggiornato) il check-in online | | `payment.completed` | Tassa di soggiorno pagata o deposito bloccato/pagato (anche quando lo staff lo segna come pagato) | | `booking.unlocked` | Tutti i passaggi sono completati: la guida del soggiorno è sbloccata | | `extras.ordered` | L'ospite ha pagato degli extra, compreso un early check-in o un late check-out | | `message.sent` | È stato inviato un messaggio all'ospite tramite il canale della prenotazione (Airbnb, Booking.com … tramite Hostaway o Channex) | | `feedback.received` | L'ospite ha valutato il soggiorno (1–5) nella pagina di feedback dopo una richiesta di recensione | | `ping` | Evento di prova dal pulsante **Test** | ### Cosa contiene `data` | Evento | Campi di `data` | |---|---| | `booking.created`, `booking.updated`, `booking.cancelled` | `bookingId`, `ref`, `source` (importazioni dai canali); `reason: "stay-times"` quando un early check-in / late check-out acquistato ha spostato gli orari | | `checkin.completed` | `bookingId`, `ref` | | `payment.completed` | `bookingId`, `leg` (`eco` o `deposit`), `status` (`paid` o `held`), `manual: true` quando lo staff lo ha segnato come pagato | | `booking.unlocked` | `bookingId` | | `extras.ordered` | `bookingId`, `leg: "extras"`, `status: "paid"` | | `message.sent` | `bookingId`, `ref`, `via` (ad es. `Hostaway`) | | `feedback.received` | `bookingId`, `ref`, `rating` (1–5) | La versione leggibile dalle macchine di ogni evento — header, body e `data` — si trova nella sezione `webhooks` del [documento OpenAPI](/api/v1/openapi.json) e nel [riferimento API](/api/docs#webhooks). ## Aggiungere un webhook **Settings → Webhooks**: inserisci un URL `https://`, spunta gli eventi che ti interessano (nessuno = tutti) e clicca su **Add webhook**. Il **signing secret** (segreto di firma) viene mostrato una sola volta: conservalo insieme al tuo ricevitore. ![Settings → Webhooks](/site/img/docs/d-settings-webhooks.webp "Settings → Webhooks") ## La richiesta `POST` con un body JSON: ``` { "id": "evt_…", "event": "payment.completed", "createdAt": "2026-11-18T14:03:11.000Z", "tenant": "t_…", "data": { "bookingId": "b_…", "leg": "eco", "status": "paid" } } ``` Header: | Header | Contenuto | |---|---| | `X-GP-Event` | il nome dell'evento | | `X-GP-Delivery` | ID univoco della consegna | | `X-GP-Timestamp` | ora Unix in secondi | | `X-GP-Signature` | `v1=` + HMAC-SHA256 esadecimale di `.` con il tuo signing secret | ## Verificare 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 sempre sul body **grezzo** della richiesta e rifiuta i timestamp vecchi per bloccare i replay. ## Verificare 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 ?? "")); } ``` Su .NET Framework usa `BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant()` al posto di `Convert.ToHexString` ed esegui il confronto a tempo costante con un semplice ciclo. ## Verificare 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'] ?? ''); ``` ## Consegna e nuovi tentativi Rispondi con un qualsiasi stato 2xx entro 10 secondi. Le consegne non riuscite vengono ritentate dopo 30 secondi e dopo 5 minuti (tre tentativi in totale). **Recent deliveries** mostra gli ultimi 200 tentativi con il relativo esito. - L'`id` nel body e l'header `X-GP-Delivery` restano uguali a ogni nuovo tentativo: salvali e ignora i duplicati. - Gli eventi possono arrivare in ordine diverso; usa `createdAt` e recupera la prenotazione per conoscerne lo stato attuale. - I redirect non vengono seguiti e vengono chiamati solo indirizzi `https://` pubblici (nessuna rete privata o locale). - Le richieste arrivano con `Content-Type: application/json` e `User-Agent: GuestPortal-Webhooks/1`. ## Domande frequenti ### Ricevo la prenotazione completa nell'evento? L'evento contiene gli ID e la modifica. Recupera la prenotazione completa con `GET /api/v1/bookings/{id}` usando una chiave API: in questo modo nessun dato personale viaggia nei body dei webhook. ### I canali possono inviare webhook a Guestbud? Sì, è la direzione opposta. Hostaway e Channex possono comunicare subito a Guestbud le prenotazioni nuove e modificate; vedi [Hostaway](/docs/connect-hostaway) e [Channex](/docs/connect-channex). Questi indirizzi in entrata sono segreti e distinti per ogni azienda e attivano solo una nuova importazione di quella prenotazione. ### L'account demo invia webhook? No.