# Webhooks Webhooks sturen events naar je systeem op het moment dat ze plaatsvinden, zodat je de API niet hoeft te pollen. ## Events | Event | Wanneer | |---|---| | `booking.created` | Er is een boeking aangemaakt (dashboard, kanaalimport of API) | | `booking.updated` | Data, gasten, in-/uitchecktijden of contactgegevens zijn gewijzigd | | `booking.cancelled` | Een boeking is geannuleerd | | `checkin.completed` | De gast heeft het online inchecken afgerond (of bijgewerkt) | | `payment.completed` | Toeristenbelasting betaald of borg gereserveerd/betaald (ook als medewerkers de betaling als betaald markeren) | | `booking.unlocked` | Alle stappen zijn afgerond: de verblijfsgids is ontgrendeld | | `extras.ordered` | De gast heeft extra's betaald, ook vroeg inchecken of laat uitchecken | | `message.sent` | Er is een bericht naar de gast gestuurd via het boekingskanaal (Airbnb, Booking.com … via Hostaway of Channex) | | `feedback.received` | De gast heeft het verblijf beoordeeld (1–5) op de feedbackpagina na een reviewverzoek | | `ping` | Testevent via de knop **Test** | ### Wat `data` bevat | Event | `data`-velden | |---|---| | `booking.created`, `booking.updated`, `booking.cancelled` | `bookingId`, `ref`, `source` (kanaalimports); `reason: "stay-times"` als een gekocht vroeg inchecken / laat uitchecken de tijden heeft verschoven | | `checkin.completed` | `bookingId`, `ref` | | `payment.completed` | `bookingId`, `leg` (`eco` of `deposit`), `status` (`paid` of `held`), `manual: true` als medewerkers de betaling als betaald hebben gemarkeerd | | `booking.unlocked` | `bookingId` | | `extras.ordered` | `bookingId`, `leg: "extras"`, `status: "paid"` | | `message.sent` | `bookingId`, `ref`, `via` (bijv. `Hostaway`) | | `feedback.received` | `bookingId`, `ref`, `rating` (1–5) | De machineleesbare versie van elk event (headers, body en `data`) staat in de sectie `webhooks` van het [OpenAPI-document](/api/v1/openapi.json) en in de [API-referentie](/api/docs#webhooks). ## Een webhook toevoegen **Settings → Webhooks**: vul een `https://`-URL in, vink de gewenste events aan (geen = alle) en klik op **Add webhook**. Het **signing secret** (ondertekeningsgeheim) wordt één keer getoond: bewaar het bij je ontvanger. ![Settings → Webhooks](/site/img/docs/d-settings-webhooks.webp "Settings → Webhooks") ## Het verzoek `POST` met een JSON-body: ``` { "id": "evt_…", "event": "payment.completed", "createdAt": "2026-11-18T14:03:11.000Z", "tenant": "t_…", "data": { "bookingId": "b_…", "leg": "eco", "status": "paid" } } ``` Headers: | Header | Inhoud | |---|---| | `X-GP-Event` | de naam van het event | | `X-GP-Delivery` | unieke bezorg-ID | | `X-GP-Timestamp` | Unix-tijd in seconden | | `X-GP-Signature` | `v1=` + hex HMAC-SHA256 van `.` met je signing secret | ## De handtekening controleren (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)); } ``` Controleer altijd tegen de **ruwe** request-body en weiger oude timestamps om replay-aanvallen te blokkeren. ## De handtekening controleren (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 ?? "")); } ``` Gebruik op .NET Framework `BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant()` in plaats van `Convert.ToHexString`, en vergelijk in constante tijd met een eenvoudige lus. ## De handtekening controleren (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'] ?? ''); ``` ## Bezorging en nieuwe pogingen Antwoord binnen 10 seconden met een willekeurige 2xx-status. Mislukte bezorgingen worden na 30 seconden en na 5 minuten opnieuw geprobeerd (in totaal drie pogingen). **Recent deliveries** toont de laatste 200 pogingen met hun resultaat. - `id` in de body en de header `X-GP-Delivery` blijven bij elke nieuwe poging gelijk: sla ze op en negeer duplicaten. - Events kunnen in een andere volgorde aankomen; gebruik `createdAt` en haal de boeking op voor de actuele status. - Redirects worden niet gevolgd en alleen openbare `https://`-adressen worden aangeroepen (geen privé- of lokale netwerken). - Verzoeken worden verstuurd met `Content-Type: application/json` en `User-Agent: GuestPortal-Webhooks/1`. ## Veelgestelde vragen ### Krijg ik de volledige boeking in het event? Het event bevat ID's en de wijziging. Haal de volledige boeking op met `GET /api/v1/bookings/{id}` met een API-sleutel; zo gaan er geen persoonsgegevens mee in webhook-bodies. ### Kunnen kanalen webhooks naar Guestbud sturen? Ja, dat is de andere richting. Hostaway en Channex kunnen Guestbud direct informeren over nieuwe en gewijzigde reserveringen; zie [Hostaway](/docs/connect-hostaway) en [Channex](/docs/connect-channex). Die inkomende adressen zijn geheim per bedrijf en starten alleen een nieuwe import van die reservering. ### Worden er webhooks verstuurd vanuit het demoaccount? Nee.