Reliable Sync with Webhooks Plus Hourly Reconciliation

Treat notifications only as a "something changed" signal and always re-fetch the data

webhooksreconciliationidempotencycancellation syncdate changes
7 min read

Introduction

If bookings only ever came in through the official site, this would be a simple story. The hard part is everything that happens afterwards: a booking arrives from an OTA, the operator shifts the dates by a day inside the channel manager, a cancellation comes through. How does all of that make it back to the official site?

This article describes how we combined change notifications from outside (webhooks) with periodic reconciliation to build sync that always finds its way back even when things drift.

Don't Trust What the Notification Says

The Reality of Webhooks Without Signatures

Most services attach a digital signature so you can verify a notification is genuine. The channel manager we used has no signing mechanism in its webhooks at all. What it offers is the ability to attach one header of your choosing.

So we set a shared secret string as that header and check it against incoming notifications. But that on its own isn't enough — if the secret ever leaks, anyone can send fake notifications.

A Notification Only Says "Something Changed"

So we flipped the approach entirely: trust nothing in the notification body, and always go fetch the data ourselves.

How to treat a webhook
BEFORE
Saving the notification contents as-is

A forged notification can rewrite booking details, and any change to the notification format breaks you

AFTER
Using the notification purely as a signal

"Something seems to have changed" triggers a re-fetch of the correct data from the official API

With that approach, even if a forged notification arrives, the only consequence is one wasted reconciliation pass. Nothing gets corrupted. We don't save or log the notification body at all — only a fingerprint of its contents (a hash) for duplicate detection.

Make It Safe for the Same Notification to Arrive Twice

Networks being what they are, the same notification turning up more than once is nothing unusual. So we record each notification's fingerprint in the database and skip recording it again if it repeats.

But we never skip the reconciliation itself. Here's why: if the dates move September 10th → 12th → back to the 10th, the first and third notifications are byte-for-byte identical. Decide "same notification, nothing to do" and you miss that change entirely.

Hourly Reconciliation Catches What Slips Through

Build for Notifications Not Arriving

Webhooks are convenient, but sometimes they just don't show up. We've seen notifications from an external service fail more than a hundred times in a row. Any design that leans on notifications alone will fall over eventually.

So we set up a job that runs every hour, pulls all currently active bookings in one go, and reconciles them against our own database. If notifications arrive, we react instantly; if they don't, we're guaranteed to catch up within the hour.

Two paths to the same sync
A change inside the channel manager

OTA booking, date change, cancellation

Webhook (the fast path)

Reacts in seconds — but sometimes doesn't arrive

Hourly reconciliation (the certain path)

Up to an hour behind — but it always catches up

Both call the same reconciliation routine
Update the booking state in our database

Apply date changes, cancellations, and reinstatements

Have both a fast path and a certain path, and have both call exactly the same reconciliation routine. That's how you get speed and certainty at the same time.

Reconciliation Only Reads

That reconciliation routine only ever reads from the channel manager. Writing back would trigger another notification for that change, with a real risk of an infinite loop.

"Sync" tends to conjure up something bidirectional, but restricting it to one direction makes the whole structure dramatically simpler.

Different Kinds of Changes Need Different Responses

Cancellations Depend on Payment Status

When something is cancelled on the channel manager side, how the official site should behave depends on whether payment has already gone through.

We don't handle cancellations of paid bookings automatically because the refund amount depends on the cancellation policy and the specifics of each case. The system's job stops at telling someone "this one needs a human."

With Date Changes, Decide What Moves and What Doesn't

When the operator changes the dates, some things should follow along and some shouldn't.

The expiry date of the guest-only page is tied to checkout, so we recalculate it automatically. The start date, though, never moves. A URL already sent to the guest suddenly reverting to "not available yet" would only cause confusion.

We don't change the amount automatically either. You shouldn't unilaterally rewrite a figure the guest already agreed to. If new dates mean a new price, that's something the operator works out with the guest.

When the Auto-Charge Date Has Already Passed

Dates sometimes get pulled forward far enough that the scheduled auto-charge date is now in the past. Charging immediately in that moment is risky — the operator may still be in the middle of adjusting the price.

So the default is not to charge, and instead email the operator asking them to review it. The exception is when check-in is today or tomorrow: there's no time to wait, so we charge right away. Overcharging someone is a mistake you can't take back, so wherever the call is genuinely uncertain, we hand it to a person.

Don't Send an Email When Nothing Changed

We email the operator with the results of reconciliation — but not on runs where there were zero differences. If "no changes" landed in their inbox every hour, the notifications that actually matter would be buried.

Conversely, the auto-charge job that runs once a day always sends its report, even when there was nothing to charge. That one doubles as a heartbeat: proof the machinery ran again today. Whether to send or stay quiet depends on what the message is for.

Wrapping Up

Three things mattered most in building sync that doesn't fall apart.

  1. Don't trust the notification contents — take it purely as a signal and re-fetch the data through the official API
  2. Have two paths — combine fast webhooks with reliable periodic reconciliation
  3. Hand judgment calls to people — don't automate refunds or price changes; flag them and notify instead