Skip to main content

Webhooks

Webhooks notify your integration when data changes, so you can keep a local copy fresh without polling. Register an HTTPS endpoint with POST /webhooks, choosing which of the event types listed by GET /webhook_events it should receive.

Events describe what changed, not the new values. A delivery names the listings affected and expects you to re-read the detail through the relevant endpoint — price recommendations, reservations or flags. This keeps payloads small and stable, and means you never have to reconcile a webhook body against an API response.

What fires each event​

Each event type has its own trigger, and they differ in how closely a delivery tracks a change to the individual listing it names. reservations.ingested and flags.detected name a listing only when that listing's own data actually changed, so a portfolio where nothing happened receives no request at all rather than an empty one. recommendations.updated is broader: its main trigger is a market being repriced, which names every active listing in that market. Read a delivery as "these listings are worth re-reading" rather than as proof that each one is different.

recommendations.updated​

Fires when a listing's price recommendations have been recomputed or posted, which is the point at which the numbers you can read may have changed. Two things cause that:

  • The nightly pricing run for a market completes. Every active listing in that market is named, because the run may have moved any of them. This is the bulk of the volume, and it is why the event batches per market — see market_id in the payload.
  • Rates are posted for a single listing, whether by automatic price posting, by POST /listings/{listing_id}/sync, or by a post from the Wheelhouse app. Only that listing is named.

Note that the nightly trigger does not compare each listing's numbers before naming it: a market being repriced names every active listing in it, including any whose recommendations ended up identical to yesterday's. So a delivery tells you a recompute covered these listings, not that every one of them moved.

It does not fire because you changed a listing's settings. A preference write that alters future recommendations is not itself this event; the recommendations move when they are next computed or posted, and the event follows then.

reservations.ingested​

Fires when the set of reservations you would read back for a listing has changed. That covers:

  • A reservation arrives that Wheelhouse did not already hold, whether picked up by a channel sync or pushed in by the integration that manages the listing's reservations.
  • A reservation Wheelhouse already held is amended — its dates, prices, payout, fees, taxes, deposits, discounts, currency, unit assignment, or booking time change.
  • A reservation is cancelled, whether the channel reports it cancelled or simply stops returning it. Cancellations detected by a reservation's disappearance are limited to stays ending within roughly the last month, so a very old reservation vanishing from a channel feed is not treated as a cancellation and does not fire.
  • A reservation is removed outright instead of being marked cancelled. Removals come from the integration that manages the listing's reservations, so a re-read finds the reservation simply gone rather than reported as Canceled.

It does not fire when a sync re-reads a reservation and finds it unchanged. Reservations are compared field by field, so a listing whose bookings are simply confirmed again each day never notifies. This is what keeps the event proportional to real booking activity rather than to sync frequency.

flags.detected​

Fires when a listing gains a flag it did not already have. Flags are the automatically derived conditions returned by GET /listings/{listing_id}/flags, and they are recomputed as a listing, its settings, or its performance change.

Only newly raised flags fire it, and the delivery names the listing rather than the flag, so re-read the flags endpoint to see which are now set. It does not fire when a flag is cleared — a listing whose last flag has just been resolved sends nothing, so do not treat the absence of deliveries as a stable flag state. If you track flags clearing as well as raising, re-read the endpoint on your own schedule in addition to reacting to this event.

Deliveries are batched. Events occurring within a five-minute window collapse into a single request whose data.listing_ids names every listing involved, so a nightly repricing run across a large portfolio arrives as a handful of requests rather than thousands. Each delivery only ever names listings you have access to.

Verify every delivery. Requests carry a Wh-Signature header of the form t=<unix timestamp>,v1=<hex>, where the hex is HMAC-SHA256(secret, "<t>.<raw request body>"). Recompute it with the subscription's secret and compare; reject the request if it does not match, or if t is older than your tolerance (five minutes is a reasonable choice) to prevent replay. The secret is returned only when the subscription is created and by POST /webhooks/\{id\}/rotate_secret — it cannot be read back afterwards.

Delivery is at-least-once, so de-duplicate. Retries reuse the same Wh-Event-Id header (also data-adjacent as the envelope id); treat a repeated id as already handled. Respond with any 2xx status as soon as you have durably accepted the request — do not do your processing before responding, since we time out after 15 seconds. Failed attempts are retried with backoff over roughly 20 minutes, and a subscription that keeps failing is disabled automatically; re-enable it with PUT /webhooks/{id} once your endpoint is healthy. Use GET /webhooks/{id}/deliveries to see what we actually sent and what your endpoint returned.