Webhooks
Noch nicht in Betrieb: das signierte JSON-Ereignis, das StayParity senden wird, wenn eine OTA den Direktpreis unterbietet. Signaturen, erneute Versuche, Tests.
Noch nicht in Betrieb
Diese Seite dokumentiert den Webhook vor dem Start. Heute erhält kein Hotel Webhooks: Befunde erreichen Hotels in der Warteschlange des Portals und in einer täglichen oder wöchentlichen Zusammenfassung per E-Mail. Die heutigen Befunde sind „Cheaper on Google“: Zwei Abrufe bei Google Hotels stimmen darin überein, dass ein Anbieter für dieselbe Nacht und dieselben Gäste einen niedrigeren Preis genannt hat. Google nennt kein Zimmer, und auf der eigenen Seite der OTA ist nichts bestätigt. Sobald der Webhook in Betrieb ist, wird er nur bestätigte Preisabweichungen senden.
Sobald der Webhook in Betrieb ist, wird StayParity jede bestätigte Preisabweichung als JSON an Ihren Server senden: eine OTA, deren eigene Seite das Zimmer des Hotels günstiger verkauft als die Website des Hotels. Damit können Sie ein Ticket anlegen, das Revenue Management des Hotels benachrichtigen oder Ihr eigenes Dashboard speisen.
Die Zustellungen werden Standard Webhooks folgen, sodass jede Standard-Webhooks-Bibliothek sie prüfen kann. Dieselbe Spezifikation steht in der OpenAPI-3.1-Beschreibung.
Einen Empfänger einrichten
Sobald der Webhook in Betrieb ist, richten Sie einen Empfänger in drei Schritten ein:
- Erstellen Sie den Signaturschlüssel. Gehen Sie im Portal zu Settings → Webhooks und wählen Sie Create key. Der Schlüssel beginnt mit
whsec_und wird nur einmal angezeigt. Kopieren Sie ihn also in die Konfiguration Ihres Empfängers, bevor Sie den Dialog schließen. Ein Schlüssel signiert die Zustellungen für alle Hotels des Kontos. - Tragen Sie Ihre URL ein. Gehen Sie mit dem gewählten Hotel zu Settings → Alerts, wählen Sie unter Where alerts go die Option Add (Set up alerts, wenn das Hotel noch keine Regel hat) und füllen Sie Webhook URL aus. Jedes Hotel hat seine eigene URL. Lassen Sie das Feld leer, um Webhooks für dieses Hotel auszuschalten.
- Antworten Sie mit einem 2xx. Prüfen Sie die Signatur, speichern Sie das Ereignis und antworten Sie schnell (siehe Erneute Versuche).
Nur Kontoadmins können den Schlüssel erstellen oder eine Webhook-URL ändern.
Die URL muss eine öffentliche https://-Adresse auf dem Standardport sein, mit höchstens 2.048 Zeichen. Wir lehnen jeden anderen Port ab, ebenso IP-Adressen, localhost, Hostnamen aus einem einzigen Label, Namen auf .local, .internal, .lan, .localhost oder .home.arpa sowie URLs, die einen Benutzernamen oder ein Passwort enthalten. Ein eigenes Token setzen Sie stattdessen in den Pfad oder den Query-String.
Wir werden nie eine unsignierte Zustellung senden. Solange das Konto keinen Schlüssel hat, bleiben Webhooks stumm, und das Portal weist neben der URL darauf hin.
Was gesendet wird
Zum Start wird es ein einziges Ereignis geben, disparity.opened. Ihr Code sollte jedes event ignorieren, das er nicht kennt, damit neue Ereignisse ihn nicht aus dem Tritt bringen.
disparity.opened
Es wird einmal gesendet, wenn eine Prüfung zum ersten Mal eine OTA findet, die für einen Aufenthalt günstiger ist als der Direktpreis des Hotels, und all das zutrifft:
- Die Differenz erfüllt die Alarmregel des Hotels: Cheaper by at least ein Betrag in Euro and at least ein Prozentsatz. Beides muss erreicht sein.
- Der Anbieter ist einer, den das Hotel überwacht.
- Der niedrigere Preis wurde auf der eigenen Seite des Anbieters bestätigt, für dasselbe Zimmer: eine Preisabweichung. Ein „Cheaper on Google“-Befund, der nur auf Abrufen bei Google Hotels beruht, sendet nie ein Ereignis.
Solange diese Preisabweichung offen bleibt, wird es nicht erneut gesendet, egal wie viele Prüfungen sie sehen. Eine Preisabweichung, die unterhalb der Schwellenwerte entstanden ist und sie später überschreitet, sendet ihr einziges Ereignis erst dann, mit reason auf now_over_threshold. Schließt sich die Preisabweichung und öffnet sich später wieder, ist das eine neue Preisabweichung mit einer neuen disparityId und einem neuen Ereignis.
Es wird verschickt, sobald die Prüfung, die die Preisabweichung gefunden hat, abgeschlossen ist. Wird die Preisabweichung behoben oder verworfen, bevor die Zustellung hinausgeht, wird es nicht gesendet.
Header
Jede Zustellung ist ein POST mit Content-Type: application/json und diesen Headern:
| Header | Wert |
|---|---|
webhook-id | Die ID der Zustellung, dieselbe wie id im Body. Bei jedem erneuten Versuch einer Zustellung gleich. |
webhook-timestamp | Wann dieser Versuch signiert wurde, in ganzen Unix-Sekunden. Bei jedem erneuten Versuch neu. |
webhook-signature | v1, gefolgt von der HMAC-SHA256-Signatur in Base64. Bei jedem erneuten Versuch neu. |
Body
| Feld | Typ | Bedeutung |
|---|---|---|
version | 3 | Die Version des Payloads. Unter ihr können Felder hinzukommen, ignorieren Sie also alle, die Sie nicht kennen. Version 2 hat confidence durch trust ersetzt und reason hinzugefügt. Version 3 hat findingKind und evidence hinzugefügt, ein Empfänger, der nur 2 akzeptiert hat, muss also jetzt 3 akzeptieren. |
id | string | Die ID der Zustellung, dieselbe wie webhook-id. Nutzen Sie sie, um Wiederholungen zu überspringen. |
event | "disparity.opened" | Was passiert ist. |
disparityId | string | Die ID der Preisabweichung in StayParity. |
hotel.id | string | Die ID des Hotels in StayParity. |
hotel.name | string | Der Name des Hotels, wie im Portal festgelegt. |
ota | string | Der Kanal als Slug in Kleinbuchstaben: booking, expedia oder der Anbieter, den Google Hotels aufgeführt hat, etwa trip oder agoda. |
stayDate | string | Die Nacht, als YYYY-MM-DD. |
occupancy | integer | Die Zahl der Gäste, 1 bis 10. |
directPriceEurMinor | integer | Der Direktpreis des Hotels, in Euro-Cent. |
otaPriceEurMinor | integer | Der Preis des Kanals, in Euro-Cent. |
deltaEurMinor | integer | Um wie viel weniger der Kanal verlangt, in Euro-Cent. |
deltaPct | number | Dieselbe Differenz als Prozentsatz des Direktpreises, auf zwei Nachkommastellen gerundet. |
trust | "confirmed" oder "seen_twice" | confirmed, wenn der Preis auf der eigenen Seite des Kanals abgerufen wurde; seen_twice, wenn zwei organische (nicht gesponserte) Abrufe bei Google Hotels im Abstand von mindestens 30 Minuten, auf derselben Grundlage, sowohl den Preis des Hotels als auch den des Anbieters innerhalb von 1 % des ersten Abrufs zeigten. |
reason | "now_over_threshold", optional | Nur vorhanden, wenn die Preisabweichung bereits unterhalb der Schwellenwerte der Regel offen war und sie inzwischen überschritten hat. Fehlt bei einer neuen Preisabweichung. |
findingKind | "rate_disparity" | Welche Art von Befund das ist. Heute immer rate_disparity: dasselbe Zimmer zu denselben Bedingungen, günstiger beim Anbieter. |
evidence.opening | object | Der erste Abruf der Preisabweichung: das Angebot des Hotels und das des Anbieters, wie unter Nachweis beschrieben. Bei einer seen_twice-Preisabweichung der erste der beiden Abrufe. |
evidence.latest | object | Der neueste Abruf der Preisabweichung, in derselben Form. Gleich opening, bis eine spätere Prüfung sie erneut abruft. |
detectedAt | string | Wann die Preisabweichung zum ersten Mal gesehen wurde, in UTC, als ISO 8601. |
Preise sind ganze Zahlen in kleinsten Währungseinheiten: die EurMinor-Felder in Euro-Cent, unabhängig von der eigenen Währung des Hotels, also ist 17900 179,00 €, und das priceMinor eines Nachweisteils in der Währung, die sein currency nennt. Teilen Sie zum Vergleichen nicht durch 100 in eine Gleitkommazahl; vergleichen Sie die ganzen Zahlen.
Nachweis
Jeder Abruf hat zwei Teile, direct (das Angebot des Hotels) und seller (das des Anbieters), mit denselben Feldern. Ein Feld, das die Quelle nicht angegeben hat, fehlt.
| Feld | Typ | Bedeutung |
|---|---|---|
priceMinor | integer | Der Preis, wie die Quelle ihn genannt hat, in kleinsten Einheiten von currency. |
currency | string | Die Währung, die die Quelle genannt hat, als ISO-4217-Code wie EUR. |
priceEurMinor | integer | Derselbe Preis, in Euro-Cent. |
source | "booking_engine", "google" oder "seller_page" | Wo er abgerufen wurde: in der Buchungsmaschine des Hotels, in der Preisliste von Google Hotels oder auf der eigenen Seite des Anbieters. |
placement | "organic" oder "ad", optional | Wo Google Hotels ihn aufgeführt hat: in seiner Preisliste oder auf einem gesponserten Platz. |
readAt | string | Wann er abgerufen wurde, in UTC, als ISO 8601. |
roomLabel | string, optional | Der Name des Zimmers, wie die Quelle ihn zeigte. |
roomRef | string, optional | Die eigene ID der Quelle für das Zimmer. |
roomType.name | string, optional | Der Zimmertyp des Hotels, dem das Zimmer zugeordnet ist, wie im Portal benannt. |
roomType.rank | integer, optional | Der Platz dieses Zimmertyps unter den Zimmertypen des Hotels, vom Einstiegszimmer aufwärts: Ein höherer Rang ist ein besseres Zimmer. |
board | "room_only" oder "breakfast", optional | Was laut Quelle im Preis enthalten ist: nur das Zimmer oder auch Frühstück. |
mappedBoard | "room_only" oder "breakfast", optional | Was laut Zimmerzuordnung des Hotels im Preis enthalten ist: beim Hotel die Verpflegung seiner Ratenkategorie, wenn die Quelle keine angegeben hat; beim Anbieter die Verpflegung der Ratenkategorie des Hotels, der die Ratenkategorie des Anbieters zugeordnet ist. |
cancellation | "free", "no_free_cancellation" oder "unknown" | Die Stornierungsbedingungen. unknown, wenn die Quelle nichts dazu gesagt hat. |
freeCancellationUntil | string, optional | Der letzte Tag für eine kostenlose Stornierung, als YYYY-MM-DD. |
guests | integer, optional | Die Zahl der Erwachsenen, für die der Preis gilt. |
taxesIncluded | boolean, optional | Ob der Preis Steuern enthält. |
link | string, optional | Wohin das Angebot führt. Bei google die Seite, auf der der Eintrag bei Google Hotels landet, die des Anbieters oder die eigene Website des Hotels: Der Preis wurde bei Google abgerufen, nicht dort. Bei booking_engine und seller_page die Seite, auf der der Preis abgerufen wurde, sofern die Quelle eine angibt. |
Beispiel
{
"version": 3,
"id": "mh7d0xq4c2v9n8b1k6t3r5w0z2y4p8s6",
"event": "disparity.opened",
"disparityId": "jn7cq2b1x9d8w4fzr3m6kt5v0h7s2e8a",
"hotel": { "id": "kd72mbx0vy4p9s1c6tqh8wf3gr5n2j7e", "name": "Hotel Aurora" },
"ota": "booking",
"stayDate": "2026-10-16",
"occupancy": 2,
"directPriceEurMinor": 18800,
"otaPriceEurMinor": 17900,
"deltaEurMinor": 900,
"deltaPct": 4.79,
"trust": "confirmed",
"findingKind": "rate_disparity",
"evidence": {
"opening": {
"direct": {
"priceMinor": 18800,
"currency": "EUR",
"priceEurMinor": 18800,
"source": "booking_engine",
"readAt": "2026-10-09T12:01:12.000Z",
"roomLabel": "Double Room with Sea View",
"mappedBoard": "breakfast",
"cancellation": "free"
},
"seller": {
"priceMinor": 17900,
"currency": "EUR",
"priceEurMinor": 17900,
"source": "seller_page",
"readAt": "2026-10-09T12:01:47.000Z",
"roomLabel": "Double Room - Sea View",
"board": "breakfast",
"cancellation": "free"
}
},
"latest": {
"direct": {
"priceMinor": 18800,
"currency": "EUR",
"priceEurMinor": 18800,
"source": "booking_engine",
"readAt": "2026-10-09T12:01:12.000Z",
"roomLabel": "Double Room with Sea View",
"mappedBoard": "breakfast",
"cancellation": "free"
},
"seller": {
"priceMinor": 17900,
"currency": "EUR",
"priceEurMinor": 17900,
"source": "seller_page",
"readAt": "2026-10-09T12:01:47.000Z",
"roomLabel": "Double Room - Sea View",
"board": "breakfast",
"cancellation": "free"
}
}
},
"detectedAt": "2026-10-09T12:02:00.000Z"
}Signatur prüfen
Prüfen Sie jede Zustellung, bevor Sie ihr vertrauen. Die Signatur beweist, dass der Body mit Ihrem Schlüssel von StayParity kam und unterwegs nicht verändert wurde. Der Zeitstempel verhindert, dass jemand eine abgefangene Zustellung erneut abspielt.
Nutzen Sie die offizielle Standard-Webhooks-Bibliothek für Ihre Sprache. Sie prüft die Signatur in konstanter Zeit und lehnt Zeitstempel ab, die mehr als fünf Minuten von Ihrer Uhr abweichen. Übergeben Sie ihr den ganzen Schlüssel, whsec_ inklusive, und den rohen Body: genau die Bytes, die angekommen sind, vor jedem JSON-Parsing. Ein Body, der geparst und wieder serialisiert wurde, passt nicht zur Signatur.
Node
npm install express standardwebhooksimport express from "express";
import { Webhook } from "standardwebhooks";
const webhook = new Webhook(process.env.STAYPARITY_WEBHOOK_SECRET);
const app = express();
// express.raw keeps the body as bytes, as it arrived.
app.post("/stayparity", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = webhook.verify(req.body.toString("utf8"), req.headers);
} catch {
return res.status(401).end();
}
// Store it and answer now; do the slow work afterwards.
res.status(204).end();
});Python
pip install flask standardwebhooksimport os
from flask import Flask, request
from standardwebhooks.webhooks import Webhook, WebhookVerificationError
app = Flask(__name__)
webhook = Webhook(os.environ["STAYPARITY_WEBHOOK_SECRET"])
@app.post("/stayparity")
def stayparity():
try:
event = webhook.verify(request.get_data(as_text=True), dict(request.headers))
except WebhookVerificationError:
return "", 401
# Store it and answer now; do the slow work afterwards.
return "", 204PHP
composer require standard-webhooks/standard-webhooks<?php
require __DIR__ . '/vendor/autoload.php';
$webhook = new \StandardWebhooks\Webhook(getenv('STAYPARITY_WEBHOOK_SECRET'));
$body = file_get_contents('php://input');
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
try {
$event = $webhook->verify($body, $headers);
} catch (\Throwable $e) {
http_response_code(401);
exit;
}
// Store it and answer now; do the slow work afterwards.
http_response_code(204);Ohne Bibliothek
Wenn Sie keine Abhängigkeit hinzufügen können, ist die Prüfung kurz:
- Nehmen Sie den Schlüssel nach
whsec_und dekodieren Sie ihn aus Base64. Diese Bytes sind der HMAC-Schlüssel. - Verbinden Sie
webhook-id,webhook-timestampund den rohen Body mit Punkten:{id}.{timestamp}.{body}. - Berechnen Sie den HMAC-SHA256 dieser Zeichenkette und kodieren Sie ihn in Base64.
webhook-signatureist eine durch Leerzeichen getrennte Liste vonv1,<signature>-Einträgen. Akzeptieren Sie die Zustellung, wenn irgendeinv1-Eintrag übereinstimmt, verglichen in konstanter Zeit. Heute senden wir einen Eintrag.- Lehnen Sie einen
webhook-timestampab, der mehr als fünf Minuten von Ihrer Uhr abweicht, in beide Richtungen.
Dieselben Schritte in TypeScript, nur mit Web Crypto, sodass der Code in Node 20+, Bun, Deno und Cloudflare Workers läuft:
// Verifies a StayParity delivery without a library. Web Crypto only, so it
// runs as is in Node 20+, Bun, Deno and Cloudflare Workers.
/** How old (or how far ahead) a delivery may be: five minutes. */
const TOLERANCE_SECONDS = 5 * 60;
/**
* Returns the parsed event, or throws. `secret` is the whole key, `whsec_`
* included; `rawBody` is the request body exactly as it arrived.
*/
export async function verifyWebhook(
secret: string,
headers: Headers,
rawBody: string,
): Promise<unknown> {
const id = headers.get("webhook-id");
const timestamp = headers.get("webhook-timestamp");
const signatures = headers.get("webhook-signature");
if (id === null || timestamp === null || signatures === null) {
throw new Error("Missing webhook headers");
}
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!(age <= TOLERANCE_SECONDS)) {
throw new Error("Timestamp outside the tolerance");
}
const key = await crypto.subtle.importKey(
"raw",
base64ToBytes(secret.replace(/^whsec_/, "")),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"],
);
const signed = new TextEncoder().encode(`${id}.${timestamp}.${rawBody}`);
// A space-separated list of `v1,<base64>`. Any one valid entry will do.
for (const entry of signatures.split(" ")) {
const [version, signature] = entry.split(",");
if (version !== "v1" || signature === undefined) continue;
let bytes: Uint8Array<ArrayBuffer>;
try {
bytes = base64ToBytes(signature);
} catch {
// A malformed entry does not invalidate another, valid signature.
continue;
}
const valid = await crypto.subtle.verify("HMAC", key, bytes, signed);
if (valid) return JSON.parse(rawBody);
}
throw new Error("No valid signature");
}
function base64ToBytes(base64: string): Uint8Array<ArrayBuffer> {
return Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
}
Erneute Versuche
Antworten Sie innerhalb von 10 Sekunden. Was wir danach tun, hängt von der Antwort ab:
| Ihre Antwort | Was passiert |
|---|---|
| Jedes 2xx | Zugestellt. Wir lesen den Body der Antwort nicht, ein leeres 200 oder 204 reicht also. |
| 5xx, ein Timeout oder ein Netzwerkfehler | Erneut versucht, bis zu 5 Versuche insgesamt. |
| 3xx | Fehlgeschlagen, kein erneuter Versuch. Weiterleitungen folgen wir nicht, geben Sie uns also die endgültige URL. |
| 4xx | Fehlgeschlagen, kein erneuter Versuch. Antworten Sie nur dann mit 4xx, wenn ein erneuter Versuch nicht helfen kann. |
Die Wartezeiten zwischen den Versuchen betragen etwa 10, 20, 40 und 80 Sekunden. Jede Wartezeit schwankt um bis zur Hälfte nach oben oder unten, damit die erneuten Versuche nicht im Gleichschritt eintreffen. Jeder Versuch wird mit einem neuen Zeitstempel neu signiert und behält dieselbe webhook-id.
Zustellungen können in beliebiger Reihenfolge ankommen, und eine Zustellung kann mehr als einmal ankommen. Sortieren Sie nach detectedAt, nicht nach Eingang, und behandeln Sie Wiederholungen wie unten beschrieben.
Die URL und der Schlüssel werden bei jedem Versuch neu gelesen. Ändern Sie die URL, während erneute Versuche ausstehen, geht der nächste Versuch an die neue.
Schlagen alle 5 Versuche fehl, endet die Zustellung, und Settings → Alerts zeigt den Grund. Sobald Ihr Server wieder funktioniert, kann ein Kontoadmin dort Retry drücken, um die neueste fehlgeschlagene Zustellung erneut zu senden, mit derselben webhook-id.
Wiederholungen behandeln
Die Zustellung erfolgt mindestens einmal. Ein Timeout auf unserer Seite kann bedeuten, dass Ihr Server die Arbeit erledigt hat und nur die Antwort verloren ging; dann kommt dieselbe Zustellung noch einmal. Auch eine Zustellung, die mit Retry im Portal erneut gesendet wird, ist eine Wiederholung.
webhook-id bleibt bei jedem erneuten Versuch einer Zustellung gleich. Bewahren Sie die IDs auf, die Sie verarbeitet haben, mindestens einige Tage lang, und antworten Sie mit 2xx, ohne die Arbeit erneut zu tun, wenn eine davon zurückkommt. Weil die ID in einem Header steht, können Sie sie prüfen, bevor Sie den Body parsen.
Wir bauen den Body bei jedem Versuch neu aus der aktuellen Preisabweichung auf, daher kann eine Wiederholung mit derselben webhook-id neuere Preise oder ein neueres trust tragen als die erste. Überspringen Sie Wiederholungen anhand von webhook-id, und wenn Sie den Body aufbewahren, behandeln Sie den zuletzt empfangenen als aktuell.
Um einmal pro Preisabweichung statt einmal pro Zustellung zu handeln, nutzen Sie disparityId als Kennung.
Schlüssel austauschen
Gehen Sie zu Settings → Webhooks und wählen Sie Replace key. Der alte Schlüssel funktioniert sofort nicht mehr; es gibt keine Übergangszeit. Der neue Schlüssel wird einmal angezeigt.
Zwischen dem Ersetzen des Schlüssels und seinem Einspielen in Ihren Empfänger werden Zustellungen mit dem neuen Schlüssel signiert, und Ihr Empfänger wird sie ablehnen. Antwortet er mit 4xx, werden diese Zustellungen nicht erneut versucht. Um sie zu behalten, setzen Sie den neuen Schlüssel sofort ein, oder lassen Sie Ihren Empfänger während des Wechsels mit 5xx antworten, wenn er eine Signatur nicht prüfen kann, damit die erneuten Versuche diese Zustellungen hinüberretten. Die erneuten Versuche erstrecken sich über etwa zweieinhalb Minuten, manchmal kaum mehr als eine, und decken daher nur einen schnellen Wechsel ab. Für eine Zustellung, die trotzdem fehlschlägt, drücken Sie Retry in Settings → Alerts, sobald der neue Schlüssel eingesetzt ist. Das sendet die neueste fehlgeschlagene Zustellung erneut.
Ersetzen Sie den Schlüssel immer dann, wenn er nach außen gelangt sein könnte: ein Commit, eine Logzeile, ein geteilter Bildschirm.
Endpunkt testen
Im Portal gibt es noch keinen Test-Button. Um Ihren Empfänger auszuprobieren, signieren Sie das Beispielereignis selbst mit Ihrem Schlüssel und senden es, genau wie wir es tun würden. Speichern Sie das Beispiel als event.json, dann:
// node send-test.mjs https://your-server.example/stayparity
import { readFileSync } from "node:fs";
import { Webhook } from "standardwebhooks";
const id = `test_${Date.now()}`;
const event = { ...JSON.parse(readFileSync("event.json", "utf8")), id };
const body = JSON.stringify(event);
const now = new Date();
const response = await fetch(process.argv[2], {
method: "POST",
headers: {
"Content-Type": "application/json",
"webhook-id": id,
"webhook-timestamp": String(Math.floor(now.getTime() / 1000)),
"webhook-signature": new Webhook(process.env.STAYPARITY_WEBHOOK_SECRET).sign(id, now, body),
},
body,
});
console.log(response.status);Prüfen Sie dann die Fehlerfälle: Ändern Sie ein Zeichen des Bodys und erwarten Sie eine Ablehnung; senden Sie dieselbe ID erneut und erwarten Sie, dass sie übersprungen wird.
Einen lokalen Empfänger machen Sie über einen Tunnel mit einem öffentlichen https://-Namen erreichbar, da wir nur an öffentliche Adressen senden.
Fehlersuche
Settings → Alerts zeigt unter der Webhook-URL jedes Hotels, wann sie zuletzt zugestellt hat oder warum die letzte Zustellung fehlgeschlagen ist:
- No signing key: Das Konto hat noch keinen Schlüssel. Erstellen Sie einen unter Settings → Webhooks.
- Rejected: Ihr Server hat mit 3xx oder 4xx geantwortet, oder die URL ist nicht mehr zulässig.
- Unreachable: 5 Versuche sind in ein Timeout gelaufen, haben mit 5xx geantwortet oder konnten keine Verbindung herstellen.
- No webhook set: Die URL wurde entfernt oder Alerting ausgeschaltet, nachdem die Zustellung zum Versand vorgemerkt wurde.
Ist die Ursache behoben, sendet Retry neben dem Fehler die neueste fehlgeschlagene Zustellung erneut.
Wenn jede Signatur fehlschlägt:
- Der Body wurde vor der Prüfung geparst. Prüfen Sie die rohen Bytes und parsen Sie danach.
- Der Schlüssel ist ein alter. Nach Replace key funktioniert nur der neue Schlüssel.
- Der Schlüssel hat beim Einfügen sein Präfix verloren oder Leerzeichen dazubekommen. Übergeben Sie der Bibliothek den ganzen
whsec_…-String. - Die Uhr Ihres Servers weicht um mehr als fünf Minuten ab. Synchronisieren Sie sie per NTP.
Wenn nichts ankommt:
- Der Webhook ist noch nicht in Betrieb. Bis dahin geht keine Zustellung hinaus.
- Die Alarmregel des Hotels ist ausgeschaltet, oder noch keine Preisabweichung hat ihre Schwellenwerte erreicht.
- Die bisherigen Preisabweichungen stammen von Anbietern, die das Hotel nicht überwacht, oder sind „Cheaper on Google“-Befunde, die nie ein Ereignis senden.
- Ihre Firewall verwirft Anfragen, die sie nicht kennt. Wir veröffentlichen keine feste Liste von Adressen, authentifizieren Sie also über die Signatur statt über die Quell-IP.
Entwicklerdoku
So verbinden Sie StayParity mit Ihren eigenen Systemen: signierte Webhooks, vor dem Start dokumentiert, und das Direktbuchungs-Badge für die Hotel-Website.
Website-Badge
Binden Sie das Direktbuchungs-Badge von StayParity mit einem Script-Tag in die Website eines Hotels ein. Snippet, Layouts, was Gäste sehen und die nötige CSP.