{"openapi":"3.1.1","info":{"title":"StayParity webhooks","version":"1","summary":"The events StayParity posts to your endpoint.","description":"StayParity posts a signed JSON event to each hotel's webhook URL when an OTA starts selling one of its rooms for less than the hotel's own website.\n\nDeliveries follow Standard Webhooks (https://www.standardwebhooks.com): verify them with a Standard Webhooks library and your whsec_ signing secret."},"externalDocs":{"description":"Webhooks guide","url":"https://stayparity.com/developers/webhooks"},"webhooks":{"disparity.opened":{"post":{"operationId":"disparityOpened","summary":"An undercut opened","description":"Sent once when an OTA is first seen selling a room for less than the hotel's direct price and the hotel's alert rule lets it through. It is not sent again while that undercut stays open.\n\nAnswer 2xx within 10 seconds. A 5xx, a timeout or a network error is retried, up to 5 attempts in all, after about 10 s, 20 s, 40 s, 80 s (each wait varies by up to half either way). A 3xx or a 4xx ends the delivery without a retry.","parameters":[{"name":"webhook-id","in":"header","required":true,"description":"The delivery's id, equal to the body's id. The same on every retry of one delivery.","schema":{"type":"string"}},{"name":"webhook-timestamp","in":"header","required":true,"description":"When this attempt was signed, in whole Unix seconds. New on every retry. Reject one more than 5 minutes from your own clock, either way.","schema":{"type":"string","pattern":"^[0-9]+$"}},{"name":"webhook-signature","in":"header","required":true,"description":"v1, a comma, then the base64 HMAC-SHA256 of \"{webhook-id}.{webhook-timestamp}.{raw body}\", keyed with the base64-decoded part of the secret after whsec_.","schema":{"type":"string","pattern":"^v1,[A-Za-z0-9+/]+={0,2}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DisparityOpened"},"example":{"version":1,"id":"mh7d0xq4c2v9n8b1k6t3r5w0z2y4p8s6","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"}}}},"responses":{"2XX":{"description":"Delivered. The response body is not read, so an empty 200 or 204 is enough."},"3XX":{"description":"Redirects are not followed. The delivery fails and is not retried."},"4XX":{"description":"Refused. The delivery fails and is not retried, so answer 4xx only when a retry cannot help."},"5XX":{"description":"Failed. Retried until 5 attempts in all."}}}}},"components":{"schemas":{"DisparityOpened":{"type":"object","properties":{"version":{"type":"number","const":1,"description":"The payload version. Fields may be added under it, so ignore any you don't know."},"id":{"type":"string","description":"The delivery's id, the same as the webhook-id header. It stays the same on every retry, so use it to skip repeats."},"event":{"type":"string","const":"disparity.opened","description":"What happened. disparity.opened is the only event today."},"disparityId":{"type":"string","description":"The undercut's id. The same undercut never sends a second event while it stays open."},"hotel":{"type":"object","properties":{"id":{"type":"string","description":"The hotel's id in StayParity."},"name":{"type":"string","description":"The hotel's name, as set in the portal."}},"required":["id","name"],"description":"The hotel the undercut is for."},"ota":{"type":"string","minLength":1,"pattern":"^[a-z0-9]+(?:_[a-z0-9]+)*$","description":"The channel, as a lower-case slug: booking, expedia, or whichever seller Google Hotels listed (trip, agoda, ...)."},"stayDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"The night, as YYYY-MM-DD."},"occupancy":{"type":"integer","minimum":1,"maximum":10,"description":"The number of guests, 1 to 10."},"directPriceEurMinor":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"The hotel's direct price, in euro cents."},"otaPriceEurMinor":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"The channel's price, in euro cents."},"deltaEurMinor":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"How much less the channel charges, in euro cents: directPriceEurMinor minus otaPriceEurMinor."},"deltaPct":{"type":"number","minimum":0,"description":"The same gap as a percentage of the direct price, rounded to two decimals."},"confidence":{"type":"string","enum":["sweep","confirmed"],"description":"confirmed if the price was read on the channel's own page, sweep if it was seen through an aggregator."},"detectedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$","description":"When the undercut was first seen, in UTC, as ISO 8601."}},"required":["version","id","event","disparityId","hotel","ota","stayDate","occupancy","directPriceEurMinor","otaPriceEurMinor","deltaEurMinor","deltaPct","confidence","detectedAt"],"description":"The body of a disparity.opened delivery."}}}}