Reaja a eventos com webhooks
Configure a URL de webhooks, passe na verificação, valide cada assinatura, confirme em até 5 s e recupere eventos perdidos com consultas periódicas.
Os webhooks enviam uma requisição HTTP POST ao seu servidor quando algo muda na sua conta: uma avaliação chega, uma listagem entra no ar, uma publicação é publicada. Há uma URL por conta e nenhuma assinatura por evento, portanto um único receptor recebe todos os 18 eventos documentados e direciona o processamento com base no campo event.
1. Defina a URL e o segredo no painel
Primeiro, implemente o receptor das etapas 2 e 3. Gerar um segredo torna a assinatura obrigatória e pausa a entrega até que seu endpoint passe pela verificação, portanto um endpoint que não esteja pronto bloqueia todos os eventos.
- Coloque o endpoint HTTPS em produção. Endereços privados são recusados e redirecionamentos não são seguidos, portanto salve a URL final sem um redirecionamento
wwwou de barra final antes dela. 2. Salve a URL na página Webhooks do painel. 3. Gere um segredo de assinatura. Ele é exibido uma única vez. Armazene-o comoLISTINGSAPI_WEBHOOK_SECRETno seu servidor. 4. Clique em Verificar endpoint. A plataforma envia o desafio da etapa 2 e aguarda até 10 segundos.
Alterar a URL ou gerar novamente o segredo redefine a verificação, portanto coloque o novo segredo em produção primeiro e depois verifique novamente. Os detalhes estão em Visão geral dos webhooks.
2. Responda ao desafio de verificação
O desafio é uma requisição POST assinada com um nome de evento reservado e um nonce. Responda com 200 e o HMAC-SHA256 apenas do nonce, em hexadecimal com letras minúsculas.
{ "event": "endpoint.verification", "nonce": "9f2c1e642b1a4b0e9f1a2c7d5e8a1b3c", "timestamp": "2026-08-25T14:32:10Z"}3. Verifique a assinatura em cada entrega
Toda requisição assinada contém X-ListingsAPI-Signature: sha256=<base64 HMAC-SHA256 of the raw body>. Calcule o mesmo resumo criptográfico sobre os bytes exatos que você recebeu, compare em tempo constante e retorne 401 se houver divergência. Não interprete e serialize novamente o JSON antes disso; isso altera os bytes. Os dois receptores abaixo preservam o corpo bruto, verificam a assinatura, respondem ao desafio e armazenam o evento de forma durável 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 });});Se uma conta nunca gerou um segredo, as entregas chegam sem assinatura. Não desenvolva sua implementação com base nesse modo; gere o segredo assim que o receptor acima estiver em produção.
4. Confirme o recebimento rapidamente e processe de forma assíncrona
Uma entrega consiste em uma única requisição POST com um tempo limite de conexão de 5 segundos e um tempo limite de leitura de 5 segundos, com exatamente uma tentativa. Não há nova tentativa, espera progressiva nem reenvio. Se o seu manipulador levar 6 segundos ou se o seu servidor ficar indisponível por 2 minutos, esses eventos serão perdidos. Apenas uma resposta 2xx faz a entrega ser considerada concluída; 3xx, 4xx e 5xx são falhas.
Portanto, o fluxo da requisição faz apenas 3 coisas: verifica a assinatura, grava o corpo bruto em uma fila ou tabela e aguarda o commit, e retorna 200. O enriquecimento, as chamadas de volta à REST API e qualquer operação que envolva terceiros são executados a partir da fila. Todos os eventos, exceto listing.submission, compartilham a mesma estrutura de 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 é uma string numérica, exceto nos eventos interaction.*, em que é repassado sem normalização e pode ser null. listing.submission não contém timestamp nem account_id e coloca status e live_link no nível superior. Os campos opcionais em data são omitidos em vez de enviados como null. Retorne 200 para qualquer event que você não reconheça; famílias não documentadas compartilham a mesma URL.
5. Elimine duplicatas e tolere entregas fora de ordem
Você pode receber o mesmo evento duas vezes e receberá o mesmo id de recurso muitas vezes: interaction.review é disparado novamente com o mesmo id de interação sempre que a avaliação muda, e nenhum payload contém uma versão. Por isso, nunca suprima eventos com base no id combinado com o nome do evento, ou você descartará edições reais. Detecte duplicatas exatas calculando o hash do corpo bruto e ignorando repetições dentro de um intervalo curto; depois, use o id do recurso como chave da operação de gravação para que uma repetição produza o mesmo 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 FalseA ordem também não é garantida. Os eventos vêm de fluxos de processamento independentes, então local_post.published pode chegar antes de local_post.created, e timestamp recebe a marca de tempo quando a entrega é preparada, não quando a alteração ocorreu. Quando a ordem for importante, consulte novamente o recurso pela API REST e confie no resultado dessa consulta. profile.updated também é limitado a um evento por local a cada 60 segundos, então uma nova consulta na chegada do evento mostra apenas a primeira edição; consulte novamente após o encerramento desse intervalo.
6. Mapeie os eventos para os outros guias
Direcione o processamento com base em event e encaminhe cada evento ao código que você escreveu para o guia correspondente. O webhook informa quando; o endpoint desse guia informa o quê.
As tabelas de campos e um exemplo de payload para cada evento estão na referência de eventos.
7. Recupere-se de um evento perdido com consultas periódicas
Como não há novas tentativas, cada família precisa de uma consulta a uma fonte confiável à qual você possa recorrer. Faça a reconciliação em intervalos programados, não apenas quando algo parecer errado, e nunca grave zero ou "sem alteração" porque um evento não chegou.
O painel registra cada tentativa com um status. success indica que a entrega foi realizada; um código HTTP, timeout, network_error ou error significa que seu endpoint foi alcançado e falhou; blocked_url, endpoint_unverified e rate_limited significam que nenhuma requisição foi feita, portanto a correção está na configuração ou no plano, não no código. A lista completa está em Comportamento de entrega.
Próximos passos
- Buscar avaliações: a primeira família que a maioria das integrações implementa, com a varredura consolidada como salvaguarda.
- Verificar o status das listagens e os links ativos: o que
listing.submissionanuncia. - Referência: Visão geral dos webhooks, Assinaturas e verificação, Comportamento de entrega, Referência de eventos.