Reagisci agli eventi con i webhook
Configura l'URL dei webhook, supera la challenge, verifica ogni firma, conferma la ricezione entro 5 secondi e recupera gli eventi persi via polling.
I webhook inviano una richiesta HTTP POST al tuo server quando qualcosa cambia nel tuo account: arriva una recensione, una scheda diventa visibile online, un post viene pubblicato. C'è un solo URL per account e non sono previste sottoscrizioni per singolo evento, quindi un unico ricevitore riceve tutti i 18 eventi documentati e li gestisce in base al campo event.
1. Imposta l'URL e il segreto nella dashboard
Crea prima il ricevitore descritto nei passaggi 2 e 3. La generazione di un segreto rende obbligatoria la firma e sospende l'invio finché il tuo endpoint non supera la verifica, quindi un endpoint non pronto blocca tutti gli eventi.
- Distribuisci l'endpoint HTTPS. Gli indirizzi privati vengono rifiutati e i reindirizzamenti non vengono seguiti, quindi salva l'URL finale senza un reindirizzamento
wwwo per la barra finale che lo preceda. 2. Salva l'URL nella pagina Webhook della dashboard. 3. Genera un segreto di firma. Viene mostrato una sola volta. Memorizzalo comeLISTINGSAPI_WEBHOOK_SECRETsul tuo server. 4. Fai clic su Verifica endpoint. La piattaforma invia il challenge del passaggio 2 e attende fino a 10 secondi.
La modifica dell'URL o la rigenerazione del segreto azzera la verifica, quindi distribuisci prima il nuovo segreto, poi esegui nuovamente la verifica. Trovi i dettagli in Panoramica dei webhook.
2. Rispondi al challenge di verifica
Il challenge è una richiesta POST firmata con un nome di evento riservato e un nonce. Rispondi con 200 e l'HMAC-SHA256 del solo nonce, in formato esadecimale con lettere minuscole.
{ "event": "endpoint.verification", "nonce": "9f2c1e642b1a4b0e9f1a2c7d5e8a1b3c", "timestamp": "2026-08-25T14:32:10Z"}3. Verifica la firma a ogni invio
Ogni richiesta firmata contiene X-ListingsAPI-Signature: sha256=<base64 HMAC-SHA256 of the raw body>. Calcola lo stesso digest sugli esatti byte ricevuti, confrontalo in tempo costante e restituisci 401 se non corrisponde. Non eseguire prima il parsing e la riserializzazione del JSON; questo modifica i byte. Entrambi i ricevitori riportati di seguito conservano il corpo grezzo, verificano la firma, rispondono al challenge e memorizzano l'evento in modo persistente prima di rispondere.
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 });});Se un account non ha mai generato un segreto, le richieste arrivano senza firma. Non basare la tua implementazione su questa modalità; genera il segreto non appena il ricevitore riportato sopra è operativo.
4. Conferma rapidamente la ricezione ed elabora in modo asincrono
Un invio consiste in una singola richiesta POST con un timeout di connessione di 5 secondi e un timeout di lettura di 5 secondi, tentata esattamente una volta. Non sono previsti nuovi tentativi, backoff o replay. Se il tuo gestore impiega sei secondi o il tuo server non è disponibile per due minuti, quegli eventi sono persi. Solo una risposta 2xx fa considerare l'invio riuscito; 3xx, 4xx e 5xx sono considerati errori.
Quindi il percorso di gestione della richiesta fa solo tre cose: verifica la firma, scrive il corpo grezzo in una coda o in una tabella e attende il commit, restituisce 200. L'arricchimento dei dati, le chiamate all'API REST e qualsiasi operazione che coinvolga terze parti vengono eseguiti a partire dalla coda. Tutti gli eventi tranne listing.submission condividono la stessa struttura del payload:
{ "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 è una stringa numerica, tranne negli eventi interaction.*, in cui viene trasmesso senza normalizzazione e può essere null. listing.submission non contiene né timestamp né account_id e colloca status e live_link al livello principale. I campi facoltativi in data vengono omessi anziché essere inviati come null. Restituisci 200 per qualsiasi event che non riconosci; le famiglie non documentate condividono lo stesso URL.
5. Elimina i duplicati e gestisci la consegna fuori ordine
Puoi ricevere lo stesso evento due volte e riceverai lo stesso id di risorsa molte volte: interaction.review viene generato di nuovo con lo stesso id dell'interazione ogni volta che la recensione cambia e nessun payload contiene una versione. Quindi non scartare mai gli eventi in base alla combinazione di id e nome dell'evento, altrimenti perderai modifiche effettive. Rileva i duplicati esatti calcolando l'hash del corpo grezzo e ignorando le ripetizioni entro un breve intervallo, quindi usa l'id della risorsa come chiave per la scrittura, in modo che una ripetizione produca lo stesso risultato.
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 FalseNemmeno l'ordine è garantito. Gli eventi provengono da pipeline indipendenti, quindi local_post.published può arrivare prima di local_post.created e timestamp viene impostato quando viene preparata la consegna, non quando si è verificata la modifica. Quando l'ordine conta, rileggi la risorsa tramite l'API REST e considera autorevole quella lettura. Inoltre, profile.updated è soggetto a debounce, con un limite di un evento per sede ogni 60 secondi, quindi una rilettura all'arrivo rileva solo la prima modifica; leggi di nuovo al termine dell'intervallo.
6. Associa gli eventi alle altre guide
Dirama la logica in base a event e passa ogni evento al codice che hai scritto per la guida corrispondente. Il webhook indica quando; l'endpoint in quella guida indica cosa.
Le tabelle dei campi e un payload di esempio per ogni evento si trovano nella documentazione di riferimento degli eventi.
7. Recupera un evento mancato tramite polling
Poiché non sono previsti nuovi tentativi, ogni famiglia ha bisogno di una lettura autorevole a cui puoi ricorrere. Esegui la riconciliazione a intervalli programmati, non solo quando qualcosa sembra non funzionare, e non scrivere mai uno zero o "nessuna modifica" perché un evento non è arrivato.
La dashboard registra ogni tentativo con uno stato. success indica che la consegna è avvenuta; un codice HTTP, timeout, network_error o error indica che il tuo endpoint è stato raggiunto e ha riscontrato un errore; blocked_url, endpoint_unverified e rate_limited indicano che non è stata effettuata alcuna richiesta, quindi la soluzione riguarda la configurazione o il piano, non il codice. L'elenco completo si trova in Comportamento della consegna.
Passaggi successivi
- Recupera le recensioni: la prima famiglia implementata dalla maggior parte delle integrazioni, con la scansione aggregata come meccanismo di riserva.
- Controlla lo stato delle schede e i link attivi: cosa annuncia
listing.submission. - Riferimenti: Panoramica dei webhook, Firme e verifica, Comportamento di consegna, Riferimento degli eventi.