---
title: "Staying in sync"
description: "Keep your copy of Kong Hotels content, rates and reservations fresh with the change feed and signed webhooks."
url: "https://portal.konghotels.cloud/guides/staying-in-sync"
image: "https://portal.konghotels.cloud/_og/d/c_Ocean.takumi,title_Staying+in+sync,description_~S2VlcCB5b3VyIGNvcHkgb2YgS29uZyBIb3RlbHMgY29udGVudCwgcmF0ZXMgYW5kIHJlc2VydmF0aW9ucyBmcmVzaCB3aXRoIHRoZSBjaGFuZ2UgZmVlZCBhbmQgc2lnbmVkIHdlYmhvb2tzLg,props_eyJ0aGVtZSI6eyJtb2RlIjoibGlnaHQiLCJjb2xvcnMiOnsicHJpbWFyeSI6IiMwRTJBMUYifX19,p_Ii9ndWlkZXMvc3RheWluZy1pbi1zeW5jIg,s_1V8JOCN7i-TTl6A8.png"
---

# Staying in sync

Don't re-crawl everything. Read the change feed or subscribe to webhooks, and refetch only what changed.

## [What to refetch](#what-to-refetch)

| Event                               | What to do                                                         |
| :---------------------------------- | :----------------------------------------------------------------- |
| availability.changed, rates.changed | Refresh that room type's dates.                                    |
| property.updated                    | Refetch /properties/{code}.                                        |
| reservation.*                       | Your own bookings changed, including changes made at the property. |

Public events (rates, availability, content, promotions and reviews) are visible to every partner. Reservation and group-request events are visible only to the partner that owns them.

## [Option 1: poll the change feed](#option-1-poll-the-change-feed)

`GET /changes` is an append-only, ordered feed of events.

1. Start from zero

Call `GET /changes?since=0` and process the events in order.

2. Store the cursor

Save `meta.next_cursor` and pass it as `since` next time. Keep calling straight away while `meta.has_more` is `true`.

3. Poll every minute or two

Narrow the feed with `types` (for example `availability.changed,rates.changed`) or `property`. `limit` goes up to 500.

```bash
curl "https://api.konghotels.cloud/changes?since=0&types=availability.changed,rates.changed&limit=100" \
  -H "apikey: $KONG_HOTELS_API_KEY"
```

## [Option 2: subscribe to webhooks](#option-2-subscribe-to-webhooks)

Create a subscription with `POST /webhooks`, and we push the same event envelope used by `/changes` to your endpoint. Send a test event with `POST /webhooks/{webhookId}/ping`, and inspect attempts with `GET /webhooks/{webhookId}/deliveries`. `GET /webhooks/event-types` lists every event type.

Each delivery is a `POST` with these headers:

| Header         | Value                                         |
| :------------- | :-------------------------------------------- |
| Kong-Event     | The event type                                |
| Kong-Delivery  | A unique delivery ID. Use it to de-duplicate. |
| Kong-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256>         |

Respond with any `2xx` within 10 seconds. Failed deliveries are retried with backoff (30 s, 2 min, 10 min, 30 min, 1 h) before being marked `failed`.

### [Verify the signature](#verify-the-signature)

Compute `HMAC_SHA256(secret, "<t>.<raw request body>")` and compare it with `v1` in constant time. Reject deliveries where `t` is more than 5 minutes old.

PHP

Node.js

Python

```php
[$t, $v1] = sscanf($_SERVER['HTTP_KONG_SIGNATURE'], 't=%d,v1=%s');
$valid = hash_equals(hash_hmac('sha256', $t.'.'.file_get_contents('php://input'), $secret), $v1)
    && abs(time() - $t) < 300;
```

```js
import crypto from 'node:crypto'

// rawBody must be the exact bytes received, before JSON parsing
function isValidKongSignature(header, rawBody, secret) {
  const { t, v1 } = Object.fromEntries(header.split(',').map((part) => part.split('=')))
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300
  return fresh && v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
}
```

```python
import hashlib, hmac, time

def is_valid_kong_signature(header: str, raw_body: bytes, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = parts["t"], parts["v1"]
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return abs(time.time() - int(t)) < 300 and hmac.compare_digest(expected, v1)
```

Rotating secrets

`POST /webhooks/{webhookId}/rotate-secret` issues a new signing secret. The secret is only returned when you create a subscription or rotate it, so store it straight away.