Skip to content
StayParity

Rate parity for booking engines and hotel groups.

We check each hotel's direct price against Booking.com, Expedia and every seller Google Hotels lists, and tell your systems when one sells for less, in a signed webhook they can act on.

POSThttps://pms.example.com/hooks/paritydisparity.opened
Content-Type: application/json
X-StayParity-Signature: t=1791547324,v1=5f0c…e21a

{
  "version": 1,
  "event": "disparity.opened",
  "disparityId": "jn7cq2b1x9d8w4fzr3m6kt5v0h7s2e8a",
  "hotel": {
    "id": "kd72mbx0vy4p9s1c6tqh8wf3gr5n2j7e",
    "name": "Hotel Aurora"
  },
  "ota": "booking",
  "stayDate": "2026-10-16",
  "occupancy": 2,
  "directPriceEurMinor": 18900,
  "otaPriceEurMinor": 17900,
  "deltaEurMinor": 1000,
  "deltaPct": 5.29,
  "confidence": "confirmed",
  "detectedAt": "2026-10-09T12:02:00.000Z"
}

Illustrative values. Hotel Aurora is fictional.

Any booking engine

Not tied to one engine. BON is the first we read directly. Hotels on any other engine are covered today.

Read from the engine

Where we have a connector for a hotel's engine, we read its direct price the way a guest sees it on the hotel's own site. BON is the first.

Or through Google Hotels

For a hotel on any other engine, we use the price Google Hotels shows for its official site. Nothing to install, and no access to the engine.

The next engine

Each engine is one connector behind the same checks, matching and alerts. If you build a booking engine or a channel manager, talk to us about being next.

The webhook

One signed event when an undercut opens. Posted as JSON to each hotel's own endpoint, and versioned from the first field.

The fields

version
Always 1 today. Fields may be added under it, so ignore any you don't know.
event
Always disparity.opened: a new undercut. It is the only event.
disparityId
The undercut's id. A retry carries the same one, so use it to skip repeats.
hotel
The hotel's id and name in StayParity.
ota
The channel, as a lower-case slug: booking, expedia, or whichever seller Google Hotels listed.
stayDate
The night, as YYYY-MM-DD.
occupancy
The guest count.
directPriceEurMinor
The hotel's direct price, in euro cents.
otaPriceEurMinor
The channel's price, in euro cents.
deltaEurMinor
How much less the channel charges, in euro cents.
deltaPct
The same gap as a percentage of the direct price, to two decimals.
confidence
confirmed if read on the channel's own page, sweep if seen through an aggregator.
detectedAt
When the undercut was first seen, in UTC.

How it's delivered

  • A POST with Content-Type application/json, to a public https URL
  • Delivered once your endpoint answers 2xx within 10 seconds
  • A 5xx or a timeout is retried, with a longer wait each time
  • A 4xx is taken as a refusal and not retried
  • Redirects are not followed, so give the final URL

Which undercuts are sent

  • Each hotel's alert rule decides: a minimum gap in euros and in percent, and which channels count
  • Confirmed undercuts only, unless the rule also takes ones seen through an aggregator
  • One event per undercut, when it opens, and not again while it stays live
  • Each hotel can post to its own endpoint, or several to the same one

The signature

HMAC-SHA256 over the timestamp and the raw body. The same shape as Stripe's, which your team may know already.

X-StayParity-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256>
  1. 1Take t, the Unix time in seconds, and v1, the signature in hex.
  2. 2Compute HMAC-SHA256 over t, a dot, then the body exactly as it arrived, keyed with your whole secret, whsec_ included.
  3. 3Compare the two in constant time, and refuse a t too far from now, so a captured request can't be sent again.
  4. 4Each retry is signed afresh, with its own t.

One signing secret per account signs every hotel's deliveries. An admin mints it in the portal and sees it once; minting a new one replaces it.

Verifying it in Node.js

JavaScript
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the request body exactly as it arrived, before JSON.parse.
// secret: your whole signing secret, whsec_ prefix included.
function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((part) => part.split("=", 2)));
  const timestamp = Number(parts.t);
  if (!Number.isInteger(timestamp) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(parts.v1, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

Accounts

Every property in one account. For a hotel group, or for the hotels you bring us as a partner.

Every hotel, its own queue

Each hotel keeps its own undercuts, alert rule and webhook endpoint, and the portal switches between them.

Your team, with roles

Admins manage the team and the signing secret. Members work the queue and set the alerts.

Set up by us

Our team adds each hotel, its alert rule and your team's access. Your hotels install nothing.

Partners

Refer your hotels, or resell StayParity. Tell us how you work with hotels, and we'll agree the terms together.

Refer

Introduce the hotels you work with. We set them up and look after them.

Resell

Offer StayParity to your hotels as part of what you already sell them.

Questions

What partners ask first. Anything else, ask us.

Do our hotels need to be on BON?
No. BON is the first engine we read directly, not a requirement. For a hotel on any other engine, we use the price Google Hotels shows for its official site.
How do we get the data?
Through the webhook: we post each new undercut to your endpoint when it opens. If you need something it doesn't carry, tell us.
What does a hotel have to install?
Nothing, for the checks and the alerts. The book-direct badge is one script tag on the hotel's own site, and only if it wants the badge.