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.

  1. 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 als LISTINGSAPI_WEBHOOK_SECRET auf 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.

JSON
{  "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.

Flask receiver
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}, 200
Express receiver
import 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:

JSON
{  "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.

Exact-duplicate check
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 False

Auch 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.

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.

EreignisWas passiert istPassende Anleitung
profile.createdEin Standort wurde erstellt. data.source ist user, api oder bulk_edit.Einen Standort hinzufügen
profile.updatedEin Standort wurde bearbeitet. data.changed_fields ist ein Hinweis, kein vollständiger Diff.Einen Standort aktualisieren
profile.deletedEin Standort wurde archiviert.Standorte suchen und archivieren
listing.submissionEine Übermittlung an eine Plattform wurde abgeschlossen: status ist success, incomplete oder canceled.Eintragsstatus und Live-Links prüfen
connection.location_connected, connection.location_disconnectedEin Google- oder Facebook-Eintrag wurde verknüpft oder seine Verknüpfung wurde aufgehoben.Google verbinden und Standorte importieren, Facebook-Seiten verbinden
connection.reauth_required, connection.listing_inaccessibleZugangsdaten sind abgelaufen, oder der Zugriff auf einen Eintrag ist verloren gegangen.Google verbinden und Standorte importieren
connection.google_verification_verified, connection.google_verification_failedEine Google-Verifizierung wurde abgeschlossen.Ein Google Business Profile erstellen
interaction.reviewEine Bewertung ist eingegangen oder wurde geändert. Die Felder befinden sich unter data.interaction.Bewertungen abrufen
interaction.responseEine Antwort des Inhabers wurde veröffentlicht, bearbeitet oder archiviert. parent_id ist die Bewertung.Auf Bewertungen antworten
local_post.created, local_post.published, local_post.rejected, local_post.deletedEin Beitragselement hat seinen Status gewechselt. Verwenden Sie data.post_item_id als Schlüssel, jeweils einen pro Standort.Beiträge veröffentlichen
review_analytics.daily_snapshot, review_analytics.weekly_snapshotDie Aggregation der Bewertungen wurde für einen Standort und einen Zeitraum ausgeführt.Bewertungsanalysen

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.

EreignisfamilieErneut lesen mitIntervall
listing.submissionGET /locations/{locationId}/listings/premium für syncStatus und listingUrl.Täglich pro Standort.
profile.*GET /locations oder GET /locations/search; updatedAt zeigt, was sich geändert hat.Stündlich sowie 60 Sekunden nach jedem profile.updated.
connection.*GET /connected-accounts und googleVerificationStatus bei jedem Lesen eines Standorts.Stündlich; während einer Verifizierung alle paar Minuten.
interaction.*GET /rollup_interactions im gesamten Konto, GET /reviewDetails anhand der ID.Alle 15 Minuten für neue Bewertungen, täglich für einen vollständigen Abgleich.
local_post.*GET /posts/{postId} oder GET /locations/{locationId}/posts.Stündlich, solange Beiträge noch verarbeitet werden.
review_analytics.*GET /locations/{locationId}/review-analytics-overview mit den Datumsangaben des Zeitraums.Täglich nach der Aggregation.

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