# Webhooks

Webhooks push events to your system the moment they happen, so you don't need to poll the API.

## Events

| Event | When |
|---|---|
| `booking.created` | A booking was created (dashboard, channel import or API) |
| `booking.updated` | Dates, guests or contact details changed |
| `booking.cancelled` | A booking was cancelled |
| `checkin.completed` | The guest completed (or updated) the online check-in |
| `payment.completed` | Tourist tax paid or deposit held/paid |
| `booking.unlocked` | All steps are done — the stay guide is unlocked |
| `extras.ordered` | The guest paid for extras |
| `message.sent` | A message was sent to the guest through the booking channel (Airbnb, Booking.com … via Hostaway or Channex) |
| `ping` | Test event from the **Test** button |

## Add a webhook

**Settings → Webhooks**: enter an `https://` URL, tick the events you want (none = all) and click **Add webhook**. The **signing secret** is shown once — store it with your receiver.

![Settings → Webhooks](/site/img/docs/d-settings-webhooks.webp "Settings → Webhooks")

## The request

`POST` with a 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 | Content |
|---|---|
| `X-GP-Event` | the event name |
| `X-GP-Delivery` | unique delivery ID |
| `X-GP-Timestamp` | Unix time in seconds |
| `X-GP-Signature` | `v1=` + hex HMAC-SHA256 of `<timestamp>.<raw body>` with your signing secret |

## Verify the signature (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));
}
```

Always verify against the **raw** request body, and reject old timestamps to block replays.

## Delivery and retries

Answer with any 2xx status within 10 seconds. Failed deliveries are retried after 30 seconds and after 5 minutes. **Recent deliveries** shows the last 200 attempts with their result. Use `id` to ignore duplicates.

## Frequently asked questions

### Do I get the full booking in the event?

The event carries IDs and the change. Fetch the full booking with `GET /api/v1/bookings/{id}` using an API key — that way no personal data travels in webhook bodies.

### Are webhooks sent from the demo account?

No.
