Mit Webhooks auf Ereignisse reagieren
Setzen Sie die Webhook-URL, bestehen Sie die Verifizierung, prüfen Sie jede Signatur, bestätigen Sie binnen 5 s und holen Sie Verpasstes per Polling nach.
Webhooks senden eine HTTP-Anfrage mit der Methode POST an Ihren Server, wenn sich in Ihrem Konto etwas ändert: Eine Bewertung geht ein, ein Eintrag geht online, ein Beitrag wird veröffentlicht. Es gibt eine URL pro Konto und keine Abonnements für einzelne Ereignisse. Daher empfängt ein Empfänger alle 18 dokumentierten Ereignisse und verzweigt anhand des Felds event.
1. URL und Signaturschlüssel im Dashboard festlegen
Implementieren Sie zuerst den Empfänger aus den Schritten 2 und 3. Das Generieren eines Signaturschlüssels macht die Signierung verpflichtend und pausiert die Zustellung, bis Ihr Endpunkt die Verifizierung besteht. Ein noch nicht einsatzbereiter Endpunkt blockiert daher jedes Ereignis.
- Stellen Sie den HTTPS-Endpunkt bereit. Private Adressen werden abgelehnt und Weiterleitungen werden nicht verfolgt. Speichern Sie daher die endgültige URL ohne vorgeschaltete
www-Weiterleitung oder Weiterleitung zum Hinzufügen eines abschließenden Schrägstrichs. 2. Speichern Sie die URL auf der Webhooks-Seite des Dashboards. 3. Generieren Sie einen Signaturschlüssel. Er wird nur einmal angezeigt. Speichern Sie ihn alsLISTINGSAPI_WEBHOOK_SECRETauf Ihrem Server. 4. Klicken Sie auf „Endpunkt verifizieren“. Die Plattform sendet die Challenge aus Schritt 2 und wartet bis zu 10 Sekunden.
Wenn Sie die URL ändern oder den Signaturschlüssel neu generieren, wird die Verifizierung zurückgesetzt. Stellen Sie daher zuerst den neuen Signaturschlüssel bereit und verifizieren Sie den Endpunkt anschließend erneut. Einzelheiten finden Sie in der Webhooks-Übersicht.
2. Die Verifizierungs-Challenge beantworten
Die Challenge ist eine signierte POST-Anfrage mit einem reservierten Ereignisnamen und einer nonce. Antworten Sie mit 200 und dem HMAC-SHA256 ausschließlich der Nonce als Hexadezimalwert in Kleinbuchstaben.
{ "event": "endpoint.verification", "nonce": "9f2c1e642b1a4b0e9f1a2c7d5e8a1b3c", "timestamp": "2026-08-25T14:32:10Z"}3. Die Signatur bei jeder Zustellung prüfen
Jede signierte Anfrage enthält X-ListingsAPI-Signature: sha256=<base64 HMAC-SHA256 of the raw body>. Berechnen Sie denselben Hashwert anhand exakt der empfangenen Bytes, vergleichen Sie ihn in konstanter Zeit und geben Sie bei einer Abweichung 401 zurück. Parsen und serialisieren Sie das JSON nicht vorher erneut; dadurch ändern sich die Bytes. Beide unten gezeigten Empfänger bewahren den unveränderten Body auf, prüfen die Signatur, beantworten die Challenge und speichern das Ereignis dauerhaft, bevor sie antworten.
import base64import hashlibimport hmacimport os from flask import Flask, abort, request app = Flask(__name__)SECRET = os.environ["LISTINGSAPI_WEBHOOK_SECRET"].encode() def signature_is_valid(raw_body: bytes, header: str | None) -> bool: digest = hmac.new(SECRET, raw_body, hashlib.sha256).digest() expected = "sha256=" + base64.b64encode(digest).decode() return hmac.compare_digest(header or "", expected) @app.post("/webhooks/listingsapi")def webhooks(): raw = request.get_data() # raw bytes, exactly as received if not signature_is_valid(raw, request.headers.get("X-ListingsAPI-Signature")): abort(401) body = request.get_json(silent=True) or {} # One-time handshake: hex digest of the nonce, not the body. if body.get("event") == "endpoint.verification": return hmac.new(SECRET, body["nonce"].encode(), hashlib.sha256).hexdigest(), 200 # Real event: durable write first, then acknowledge. Swap for your queue. inbox.put(raw) # must be committed before the 200 goes out return {"received": True}, 200import crypto from 'crypto';import express from 'express'; const app = express();const SECRET = process.env.LISTINGSAPI_WEBHOOK_SECRET as string; // express.raw keeps req.body as the untouched Buffer the HMAC covers.app.post('/webhooks/listingsapi', express.raw({ type: '*/*' }), async (req, res) => {const raw: Buffer = req.body;const digest = crypto.createHmac('sha256', SECRET).update(raw).digest('base64');const expected = Buffer.from('sha256=' + digest);const given = Buffer.from(req.get('X-ListingsAPI-Signature') ?? '');if (given.length !== expected.length || !crypto.timingSafeEqual(given, expected)) { return res.status(401).send('bad signature');} const body = JSON.parse(raw.toString('utf8')); // One-time handshake: hex digest of the nonce, not the body.if (body.event === 'endpoint.verification') { const answer = crypto.createHmac('sha256', SECRET).update(body.nonce).digest('hex'); return res.status(200).send(answer);} // Real event: durable write first, then acknowledge. Swap for your queue.await inbox.put(raw); // must be committed before the 200 goes outreturn res.status(200).json({ received: true });});Wenn für ein Konto noch nie ein Signaturschlüssel generiert wurde, treffen Zustellungen unsigniert ein. Bauen Sie nicht auf diesem Modus auf; generieren Sie den Signaturschlüssel, sobald der oben gezeigte Empfänger in Betrieb ist.
4. Schnell bestätigen und asynchron verarbeiten
Eine Zustellung besteht aus einer einzigen POST-Anfrage mit einem Verbindungs-Timeout von 5 Sekunden und einem Lese-Timeout von 5 Sekunden und wird genau einmal versucht. Es gibt weder Wiederholungsversuche noch Backoff noch eine erneute Auslieferung. Wenn Ihr Handler 6 Sekunden benötigt oder Ihr Server 2 Minuten lang ausfällt, gehen diese Ereignisse verloren. Nur 2xx zählt als erfolgreiche Zustellung; 3xx, 4xx und 5xx gelten als Fehler.
Die Anfrageverarbeitung erledigt daher nur drei Dinge: die Signatur prüfen, den unveränderten Body in eine Warteschlange oder Tabelle schreiben und auf den Commit warten sowie 200 zurückgeben. Datenanreicherung, Rückaufrufe an die REST API und sämtliche Interaktionen mit Drittanbietern werden über die Warteschlange ausgeführt. Alle Ereignisse außer listing.submission verwenden dieselbe Payload-Struktur:
{ "event": "connection.location_connected", "timestamp": "2026-08-25T14:32:10Z", "account_id": 11073, "location_id": "279381", "data": { "platform": "google", "connected_account_id": "ba03bc4b-9f8c-4d3b-8e93-9628034c63cc" }}location_id ist eine Zeichenfolge aus Ziffern, außer bei interaction.*-Ereignissen, bei denen der Wert ohne Normalisierung durchgereicht wird und null sein kann. listing.submission enthält weder timestamp noch account_id und platziert status und live_link auf der obersten Ebene. Optionale Felder in data werden weggelassen, statt als null gesendet zu werden. Geben Sie für jeden unbekannten Wert von event den Status 200 zurück; nicht dokumentierte Ereignisfamilien verwenden dieselbe URL.
5. Duplikate erkennen und Zustellung in abweichender Reihenfolge tolerieren
Sie können dasselbe Ereignis zweimal empfangen, und Sie werden dieselbe Ressourcen-ID viele Male empfangen: interaction.review wird bei jeder Änderung der Bewertung mit derselben id der Interaktion erneut ausgelöst, und keine Payload enthält eine Version. Unterdrücken Sie daher niemals Ereignisse anhand der Kombination aus ID und Ereignisname, sonst verwerfen Sie tatsächliche Änderungen. Erkennen Sie exakte Duplikate, indem Sie den unverarbeiteten Body hashen und eine Wiederholung innerhalb eines kurzen Zeitfensters ignorieren. Verwenden Sie anschließend die Ressourcen-ID als Schlüssel für Ihren Schreibvorgang, damit eine Wiederholung dasselbe Ergebnis erzeugt.
import hashlibimport time SEEN: dict[str, float] = {} # use Redis with a TTL in productionWINDOW_SECONDS = 600 def is_duplicate(raw_body: bytes) -> bool: key = hashlib.sha256(raw_body).hexdigest() now = time.time() for k, seen_at in list(SEEN.items()): if now - seen_at > WINDOW_SECONDS: del SEEN[k] if key in SEEN: return True SEEN[key] = now return FalseAuch die Reihenfolge ist nicht garantiert. Ereignisse stammen aus unabhängigen Pipelines, sodass local_post.published vor local_post.created eintreffen kann, und timestamp wird beim Erstellen der Zustellung gesetzt, nicht zum Zeitpunkt der Änderung. Wenn die Reihenfolge wichtig ist, lesen Sie die Ressource erneut über die REST API und verlassen Sie sich auf dieses Leseergebnis. profile.updated wird außerdem durch Debouncing auf ein Ereignis pro Standort und 60 Sekunden begrenzt, sodass ein erneutes Lesen beim Eintreffen nur die erste Änderung erfasst; lesen Sie nach Ablauf des Zeitfensters erneut.
6. Ereignisse den anderen Anleitungen zuordnen
Verzweigen Sie anhand von event und übergeben Sie jedes Ereignis an den Code, den Sie für die passende Anleitung geschrieben haben. Der Webhook sagt, wann etwas passiert ist; der Endpunkt in dieser Anleitung sagt, was passiert ist.
Feldtabellen und eine Beispiel-Payload für jedes Ereignis finden Sie in der Ereignisreferenz.
7. Ein verpasstes Ereignis durch Polling ausgleichen
Da es keinen erneuten Zustellversuch gibt, benötigt jede Ereignisfamilie einen maßgeblichen Lesezugriff, auf den Sie zurückgreifen können. Gleichen Sie die Daten nach einem Zeitplan ab, nicht nur dann, wenn etwas falsch erscheint, und schreiben Sie niemals eine Null oder „keine Änderung“, weil ein Ereignis nicht eingetroffen ist.
Das Dashboard erfasst jeden Versuch mit einem Status. success bedeutet zugestellt; ein HTTP-Code, timeout, network_error oder error bedeutet, dass Ihr Endpunkt erreicht wurde und ein Fehler aufgetreten ist; blocked_url, endpoint_unverified und rate_limited bedeuten, dass überhaupt keine Anfrage gesendet wurde, sodass die Lösung in der Konfiguration oder im Tarif liegt, nicht im Code. Die vollständige Liste finden Sie unter Zustellverhalten.
Nächste Schritte
- Bewertungen abrufen: die erste Funktionsfamilie, die die meisten Integrationen implementieren, mit dem Rollup-Durchlauf als Absicherung.
- Status der Einträge und Live-Links prüfen: was
listing.submissionankündigt. - Referenz: Webhooks-Übersicht, Signaturen und Verifizierung, Zustellungsverhalten, Ereignisreferenz.