Reacciona a los eventos con webhooks
Fija tu URL de webhooks, supera el reto de verificación, valida cada firma, confirma en 5 segundos y recupera eventos perdidos con consultas periódicas.
Los webhooks envían una solicitud HTTP POST a tu servidor cuando algo cambia en tu cuenta: llega una reseña, una ficha se activa o se publica una publicación. Hay una URL por cuenta y no hay suscripciones por evento, por lo que un único receptor recibe los 18 eventos documentados y decide qué lógica ejecutar según el campo event.
1. Configura la URL y el secreto en el panel
Primero, implementa el receptor de los pasos 2 y 3. Generar un secreto hace que la firma sea obligatoria y pausa las entregas hasta que tu endpoint supere la verificación, por lo que un endpoint que no esté listo bloquea todos los eventos.
- Despliega el endpoint HTTPS. Se rechazan las direcciones privadas y no se siguen las redirecciones, así que guarda la URL final sin ninguna redirección
wwwni redirección por una barra final antes de llegar a ella. 2. Guarda la URL en la página Webhooks del panel. 3. Genera un secreto de firma. Se muestra una sola vez. Almacénalo comoLISTINGSAPI_WEBHOOK_SECRETen tu servidor. 4. Haz clic en Verificar endpoint. La plataforma envía el desafío del paso 2 y espera hasta 10 segundos.
Cambiar la URL o regenerar el secreto restablece la verificación, así que primero despliega el nuevo secreto y luego vuelve a verificar. Encontrarás los detalles en Descripción general de los webhooks.
2. Responde al desafío de verificación
El desafío es una solicitud POST firmada con un nombre de evento reservado y un nonce. Responde con 200 e incluye el HMAC-SHA256 calculado únicamente sobre el nonce, en hexadecimal con letras minúsculas.
{ "event": "endpoint.verification", "nonce": "9f2c1e642b1a4b0e9f1a2c7d5e8a1b3c", "timestamp": "2026-08-25T14:32:10Z"}3. Verifica la firma en cada entrega
Cada solicitud firmada incluye X-ListingsAPI-Signature: sha256=<base64 HMAC-SHA256 of the raw body>. Calcula el mismo resumen criptográfico sobre los bytes exactos que recibiste, compara en tiempo constante y devuelve 401 si no coinciden. No analices ni vuelvas a serializar el JSON antes de hacerlo; eso cambia los bytes. Los dos receptores siguientes conservan el cuerpo sin procesar, verifican la firma, responden al desafío y almacenan el evento de forma duradera antes de responder.
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 });});Si una cuenta nunca ha generado un secreto, las entregas llegan sin firma. No bases tu implementación en ese modo; genera el secreto una vez que el receptor anterior esté en funcionamiento.
4. Confirma la recepción rápidamente y procesa de forma asíncrona
Una entrega consiste en una solicitud POST con un tiempo de espera máximo de conexión de 5 segundos y un tiempo de espera máximo de lectura de 5 segundos, que se intenta exactamente una vez. No hay reintentos, esperas progresivas ni reenvíos. Si tu manejador tarda seis segundos o tu servidor está caído durante dos minutos, esos eventos se pierden. Solo una respuesta 2xx cuenta como entrega correcta; 3xx, 4xx y 5xx son fallos.
Por tanto, el flujo de la solicitud solo hace tres cosas: verificar, escribir el cuerpo sin procesar en una cola o tabla y esperar a que se confirme la escritura, y devolver 200. El enriquecimiento de datos, las llamadas de vuelta a la API REST y cualquier operación que interactúe con un tercero se ejecutan desde la cola. Todos los eventos excepto listing.submission comparten la misma estructura de datos:
{ "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 es una cadena numérica, excepto en los eventos interaction.*, donde se transmite sin normalizar y puede ser null. listing.submission no tiene timestamp ni account_id y coloca status y live_link en el nivel superior. Los campos opcionales de data se omiten en lugar de enviarse como null. Devuelve 200 para cualquier event que no reconozcas; las familias no documentadas comparten la misma URL.
5. Elimina duplicados y admite la entrega fuera de orden
Puedes recibir el mismo evento dos veces y recibirás el mismo id de recurso muchas veces: interaction.review vuelve a emitirse con la misma interacción id cada vez que cambia la reseña, y ningún cuerpo de datos incluye una versión. Por eso, nunca descartes eventos basándote en el id y el nombre del evento, o perderás modificaciones reales. Detecta duplicados exactos calculando un hash del cuerpo sin procesar e ignorando las repeticiones dentro de un intervalo breve; después, usa el id del recurso como clave de escritura para que una repetición produzca el mismo resultado.
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 FalseEl orden tampoco está garantizado. Los eventos proceden de flujos de procesamiento independientes, por lo que local_post.published puede llegar antes que local_post.created, y timestamp se registra cuando se prepara la entrega, no cuando se produjo el cambio. Cuando el orden importe, vuelve a consultar el recurso en la API REST y confía en esa lectura. Además, profile.updated limita las emisiones a un evento por ubicación cada 60 segundos, por lo que una consulta al recibirlo solo muestra la primera modificación; vuelve a consultar cuando termine ese intervalo.
6. Asocia los eventos con las otras guías
Selecciona la rama de ejecución según event y pasa cada evento al código que escribiste para la guía correspondiente. El webhook indica cuándo; el endpoint de esa guía indica qué.
Las tablas de campos y un cuerpo de datos de ejemplo para cada evento se encuentran en la referencia de eventos.
7. Recupérate de un evento perdido mediante consultas periódicas
Como no hay reintentos, cada familia necesita una consulta a una fuente de datos fiable a la que puedas recurrir. Concilia los datos de forma programada, no solo cuando algo parezca incorrecto, y nunca escribas un cero o "sin cambios" porque no haya llegado un evento.
El panel registra cada intento con un estado. success indica que se entregó; un código HTTP, timeout, network_error o error significa que se llegó a tu endpoint y se produjo un fallo; blocked_url, endpoint_unverified y rate_limited significan que no se realizó ninguna solicitud, por lo que la solución está en la configuración o el plan, no en el código. La lista completa se encuentra en Comportamiento de entrega.
Próximos pasos
- Obtener reseñas: la primera familia que implementa la mayoría de las integraciones, con el barrido consolidado como respaldo.
- Comprobar el estado de las fichas y los enlaces activos: lo que anuncia
listing.submission. - Referencia: Descripción general de los webhooks, Firmas y verificación, Comportamiento de entrega, Referencia de eventos.