Webhooks e Eventos
Esta documentação explica como receber notificações em tempo real sobre eventos que acontecem no Garagem através de webhooks.
📡 Webhooks de Canais (Inbound Leads)
Seção intitulada “📡 Webhooks de Canais (Inbound Leads)”Os webhooks de canais permitem que você envie leads diretamente para o Garagem através de endpoints configuráveis. É a forma principal de integrar sites, landing pages e sistemas externos.
Simétrico para o sentido contrário: o mesmo
pathTokentambém entrega imóveis filtrados em JSON via Listings por Canal (Feed JSON) — autenticado por Bearer token do canal, com suporte a slots/destaque.
Endpoint
Seção intitulada “Endpoint”POST https://app.garagem.ai/api/webhooks/channels/{pathToken}O pathToken é um token único gerado para cada canal de marketing. Você pode encontrá-lo nas configurações do canal no Garagem (ex: chnl_2xxxxxxxxxx).
Autenticação
Seção intitulada “Autenticação”Para o webhook padrão do canal, configure o segredo do webhook na seção Recebimento da integração. O mesmo valor autentica de duas formas — envie uma delas:
- Bearer Token — header
Authorization: Bearer <segredo> - HMAC Signature — header
x-hmac-sha256(oux-signature) com o HMAC-SHA256 hexadecimal do body cru. Prefixosha256=é aceito.
Se o segredo estiver vazio, a autenticação extra é opcional (o pathToken na URL continua opaco).
Alguns provedores usam métodos próprios além disso:
- API Key / header customizado — canais API (ex.: RD Station)
- HTTP Basic — GrupoZap, ChavesNaMão
- HMAC do provedor — Meta Lead Ads e similares
Destino da Requisição
Seção intitulada “Destino da Requisição”Por padrão, os leads são criados como negociações (deals) — ou seja, um lead de comprador/inquilino. Você pode alterar o destino para listing (anúncio de proprietário) de duas formas:
Via header:
x-inbound-destination: listingOu via body:
{ "destination": "listing"}Nota: O destino precisa estar habilitado na configuração do canal. Se o canal permitir apenas
deal, enviarlistingretornará erro 403.
Resposta
Seção intitulada “Resposta”Por padrão o processamento é assíncrono: a API responde imediatamente com 202 e processa o lead em background.
{ "ok": true, "accepted": true}Para receber dealId / listingId na resposta, use ?sync=true (ou ?sync=deal, ou leadIntakeConfig.syncResponse = true no canal). Isso espera a criação da negociação, não a atribuição do corretor.
{ "ok": true, "status": "created", "dealId": "uuid-do-deal-criado", "listingId": null, "assignedToId": null, "idempotent": false}Se o destino for listing:
{ "ok": true, "status": "created", "dealId": null, "listingId": "uuid-do-listing-criado", "assignedToId": null}Para esperar também o corretor responsável, use ?sync=assignment. Se o canal não tiver fila de distribuição, a resposta ainda volta com dealId e assignedToId: null.
Se a primeira entrega ainda está em voo, o reenvio responde 202 com status: "processing" (e dealId se já estiver persistido). Depois de processado, o mesmo originLeadId responde 200 com o dealId já criado:
{ "ok": true, "status": "duplicate", "duplicate": true, "idempotent": true, "dealId": "uuid-do-deal-criado", "listingId": null, "assignedToId": null}Campos Suportados
Seção intitulada “Campos Suportados”O webhook aceita os campos abaixo no body JSON. Todos são opcionais — envie apenas o que tiver disponível.
Contato (nomes flexíveis)
Seção intitulada “Contato (nomes flexíveis)”| Campo | Aliases aceitos | Descrição |
|---|---|---|
contactName | name, ownerName | Nome do contato |
contactEmail | email, ownerEmail | E-mail do contato |
contactPhone | phone, ownerPhone | Telefone (formato E.164: +5511999999999) |
cpf | ownerCpf | CPF (opcional) |
Referência ao Imóvel
Seção intitulada “Referência ao Imóvel”| Campo | Descrição |
|---|---|
listingId | ID (UUID) do anúncio no Garagem ou código do catálogo (ex: 423699). Quando for UUID, o Garagem extrai automaticamente os dados do imóvel |
listingCode | Código de referência do anúncio (ex: 423699, AP-001). Alternativa ao UUID — resolve o imóvel pelo código do catálogo |
propertyId | Alternativa ao listingId — UUID do imóvel/anúncio, ou código do catálogo se não for UUID |
clientListingId | Alias de código/referência (mesmo efeito de listingCode) |
Dica: Se você tem o UUID do anúncio, envie
listingId. Se só tem o código do catálogo (Vista, site próprio, etc.), envielistingCode— não é necessário traduzir código → UUID antes.Quando o anúncio é encontrado, o Garagem preenche automaticamente os dados do imóvel (tipo, quartos, bairro, preço, etc.). Não é necessário enviar os campos de imóvel manualmente.
Tipo de Negócio
Seção intitulada “Tipo de Negócio”| Campo | Valores | Descrição |
|---|---|---|
businessType | sale, rent, rental, both | Tipo de negócio. Se não enviado, assume sale |
O businessType define o tipo da negociação criada:
sale→ Negociação de vendarentourental→ Negociação de locaçãoboth→ Venda e locação
Dados do Imóvel (opcionais)
Seção intitulada “Dados do Imóvel (opcionais)”Se você não tiver o listingId, pode enviar os dados diretamente:
| Campo | Tipo | Descrição |
|---|---|---|
propertyType | string | apartment, house, land, commercial, etc. |
bedrooms | number | Quartos |
bathrooms | number | Banheiros |
suites | number | Suítes |
parkingSpaces | number | Vagas |
totalArea | number | Área total (m²) |
usableArea | number | Área útil (m²) |
price | number | Valor do imóvel |
condominiumFee | number | Valor do condomínio |
Endereço
Seção intitulada “Endereço”{ "address": { "street": "Avenida Paulista", "number": "1578", "complement": "Apto 42", "neighborhood": "Bela Vista", "city": "São Paulo", "state": "SP", "postalCode": "01310-200", "fullAddress": "Av. Paulista, 1578 - Bela Vista, São Paulo - SP" }}Contexto do Lead
Seção intitulada “Contexto do Lead”| Campo | Descrição |
|---|---|
message | Mensagem do lead (ex: “Tenho interesse neste imóvel”) |
source | Origem do lead (ex: “Site”, “Facebook Ads”) |
campaign | Nome da campanha |
leadType | Tipo do lead: visit, contact (interesse do comprador), seller_lead (captação / anunciar imóvel). Alias legado de captação: property |
Agendamento de Visita
Seção intitulada “Agendamento de Visita”| Campo | Formato | Descrição |
|---|---|---|
visitDate | YYYY-MM-DD | Data da visita |
visitTime | HH:mm | Horário da visita |
visitNotes | string | Observações sobre a visita |
Quando visitDate e visitTime são enviados, o Garagem cria automaticamente uma tarefa de visita vinculada à negociação.
UTM / Atribuição de Marketing
Seção intitulada “UTM / Atribuição de Marketing”| Campo | Descrição |
|---|---|
utmSource | Origem (ex: google, facebook) |
utmMedium | Meio (ex: cpc, social, email) |
utmCampaign | Campanha |
utmTerm | Termo de busca |
utmContent | Conteúdo do anúncio |
referrer | URL absoluta de referência (https://www.google.com). Um valor que não é URL é ignorado e não impede o lead. |
landingPage | Página de destino: URL absoluta (https://www.example.com/imoveis/123) ou caminho começando com / (/imoveis/123, /). Se o payload também trouxer siteDomain, o caminho vira URL absoluta. Um valor inválido é ignorado e não impede o lead. |
gclid | Google Click ID |
Controle e Idempotência
Seção intitulada “Controle e Idempotência”| Campo | Descrição |
|---|---|
destination | deal ou listing — controle de destino via body |
originLeadId | ID único do lead na origem — evita duplicatas. Se enviado duas vezes com o mesmo valor, o segundo retorna a negociação existente (status: "duplicate") |
externalId | Referência externa (para rastreabilidade) |
Exemplos Práticos
Seção intitulada “Exemplos Práticos”1. Lead Mínimo (Contato Simples)
Seção intitulada “1. Lead Mínimo (Contato Simples)”O exemplo mais simples — apenas nome, telefone e mensagem:
curl -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sua_api_key" \ "https://app.garagem.ai/api/webhooks/channels/chnl_seu_token_aqui" \ -d '{ "contactName": "João Silva", "contactPhone": "+5511999999999", "message": "Tenho interesse em imóveis na região" }'2. Lead com Referência ao Imóvel (Recomendado)
Seção intitulada “2. Lead com Referência ao Imóvel (Recomendado)”Quando você tem o ID (UUID) ou o código do anúncio, envie listingId / listingCode — o Garagem resolve o imóvel e extrai automaticamente tipologia, bairro, valor, fotos, etc.:
Por UUID:
curl -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sua_api_key" \ "https://app.garagem.ai/api/webhooks/channels/chnl_seu_token_aqui" \ -d '{ "contactName": "João Silva", "contactEmail": "[email protected]", "contactPhone": "+5511999999999", "listingId": "uuid-do-anuncio", "message": "Tenho interesse neste imóvel" }'Por código do catálogo (sem traduzir para UUID):
curl -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sua_api_key" \ "https://app.garagem.ai/api/webhooks/channels/chnl_seu_token_aqui" \ -d '{ "contactName": "João Silva", "contactEmail": "[email protected]", "contactPhone": "+5511999999999", "listingCode": "423699", "message": "Tenho interesse neste imóvel" }'Ideal para sites onde o lead vem de uma página de imóvel específica. Use o UUID quando disponível; caso contrário, o código que aparece no catálogo/URL (
listingCode) é suficiente.
3. Lead com Tipo de Negócio (Locação ou Venda)
Seção intitulada “3. Lead com Tipo de Negócio (Locação ou Venda)”Especifique businessType para diferenciar leads de venda e locação:
Locação:
curl -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sua_api_key" \ "https://app.garagem.ai/api/webhooks/channels/chnl_seu_token_aqui" \ -d '{ "contactName": "Maria Santos", "contactEmail": "[email protected]", "contactPhone": "+5511888888888", "businessType": "rental", "listingId": "uuid-do-anuncio", "message": "Gostaria de alugar este imóvel" }'Venda:
curl -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sua_api_key" \ "https://app.garagem.ai/api/webhooks/channels/chnl_seu_token_aqui" \ -d '{ "contactName": "Carlos Oliveira", "contactEmail": "[email protected]", "contactPhone": "+5511777777777", "businessType": "sale", "listingId": "uuid-do-anuncio", "message": "Quero comprar este apartamento" }'Dica: Em sites que diferenciam locação e venda por URL (ex:
/aluguel/...e/venda/...), extraia obusinessTypea partir da URL e envie no payload.
4. Lead com Detalhes do Imóvel (Sem listingId)
Seção intitulada “4. Lead com Detalhes do Imóvel (Sem listingId)”Quando você não tem o listingId, envie os dados do imóvel diretamente:
curl -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sua_api_key" \ "https://app.garagem.ai/api/webhooks/channels/chnl_seu_token_aqui" \ -d '{ "contactName": "Ana Lima", "contactEmail": "[email protected]", "contactPhone": "+5511666666666", "propertyType": "apartment", "bedrooms": 3, "bathrooms": 2, "price": 750000, "businessType": "sale", "address": { "neighborhood": "Bela Vista", "city": "São Paulo", "state": "SP" }, "message": "Busco apartamento de 3 quartos nesta região" }'5. Lead com Agendamento de Visita
Seção intitulada “5. Lead com Agendamento de Visita”Envie visitDate e visitTime para criar automaticamente uma tarefa de visita:
curl -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sua_api_key" \ "https://app.garagem.ai/api/webhooks/channels/chnl_seu_token_aqui" \ -d '{ "contactName": "Pedro Costa", "contactEmail": "[email protected]", "contactPhone": "+5511555555555", "listingId": "uuid-do-anuncio", "businessType": "rental", "message": "Gostaria de visitar o imóvel", "visitDate": "2026-03-25", "visitTime": "14:00", "visitNotes": "Prefiro horário à tarde" }'6. Lead Completo com Atribuição de Marketing
Seção intitulada “6. Lead Completo com Atribuição de Marketing”Exemplo com todos os campos de rastreamento UTM:
curl -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sua_api_key" \ "https://app.garagem.ai/api/webhooks/channels/chnl_seu_token_aqui" \ -d '{ "contactName": "João Silva", "contactEmail": "[email protected]", "contactPhone": "+5511999999999", "listingId": "uuid-do-anuncio", "businessType": "sale", "message": "Vi o anúncio no Google e tenho interesse", "source": "Google Ads", "campaign": "Lançamento Centro SP", "utmSource": "google", "utmMedium": "cpc", "utmCampaign": "lancamento-centro-2026", "utmTerm": "apartamento 3 quartos bela vista", "utmContent": "ad-variant-a", "gclid": "CjwKCAjw...", "landingPage": "/imoveis/apartamento-bela-vista-123", "referrer": "https://www.google.com" }'landingPage nesse exemplo é um caminho. O mesmo campo também aceita URL absoluta:
"landingPage": "https://www.example.com/imoveis/apartamento-bela-vista-123"referrer é só URL absoluta, como "https://www.google.com".
7. Captação de Imóvel (Destino: Listing)
Seção intitulada “7. Captação de Imóvel (Destino: Listing)”Para leads de proprietários que querem anunciar um imóvel, use destination: "listing":
curl -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sua_api_key" \ -H "x-inbound-destination: listing" \ "https://app.garagem.ai/api/webhooks/channels/chnl_seu_token_aqui" \ -d '{ "contactName": "Roberto Almeida", "contactEmail": "[email protected]", "contactPhone": "+5511444444444", "propertyType": "house", "bedrooms": 4, "bathrooms": 3, "suites": 2, "parkingSpaces": 3, "totalArea": 250, "price": 1200000, "businessType": "sale", "address": { "street": "Rua das Palmeiras", "number": "456", "neighborhood": "Jardim Europa", "city": "São Paulo", "state": "SP", "postalCode": "01438-000" }, "message": "Quero anunciar minha casa para venda" }'Também é possível enviar
"destination": "listing"no body em vez do header.
8. Lead com Idempotência (Evitar Duplicatas)
Seção intitulada “8. Lead com Idempotência (Evitar Duplicatas)”Use originLeadId para evitar duplicatas quando o mesmo lead for enviado mais de uma vez:
curl -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sua_api_key" \ "https://app.garagem.ai/api/webhooks/channels/chnl_seu_token_aqui" \ -d '{ "contactName": "João Silva", "contactEmail": "[email protected]", "contactPhone": "+5511999999999", "listingId": "uuid-do-anuncio", "message": "Tenho interesse neste imóvel", "originLeadId": "meu-sistema-lead-12345" }'Se o mesmo
originLeadIdfor enviado novamente para o mesmo canal, o Garagem não cria outra negociação. Com?sync=true, a resposta inclui odealIdoriginal estatus: "duplicate". Sem sync, o padrão continua202 { "ok": true, "accepted": true }na primeira entrega.
🎯 Eventos do Sistema
Seção intitulada “🎯 Eventos do Sistema”O Garagem emite eventos quando entidades são criadas, atualizadas ou deletadas. Você pode configurar webhooks para receber notificações sobre esses eventos.
Autenticação dos eventos de saída
Seção intitulada “Autenticação dos eventos de saída”Cada POST sai sem autenticação até você ligar uma opção em Configurações → Desenvolvedor → Webhooks. Dá para ligar as duas ao mesmo tempo. As duas usam o mesmo segredo, gerado pelo Garagem (32 bytes, base64url). Enquanto nenhuma estiver ligada, os headers de autenticação não são enviados: um endpoint que já recebe o webhook continua igual.
Não há lista fixa de IPs de saída. O tráfego sai da rede compartilhada de execução do Garagem, então uma allowlist de IP no seu servidor não identifica a origem. Use HMAC, Bearer, ou os dois.
| Opção | O que ligar | Headers adicionados |
|---|---|---|
| HMAC-SHA256 | Assinar com HMAC | x-webhook-timestamp, x-webhook-signature |
| Bearer | Enviar token Authorization | Authorization: Bearer <segredo> |
| As duas | Os dois interruptores | Os três headers, com o mesmo segredo |
| Nenhuma | Padrão | Nenhum header de autenticação |
O botão Testar envia o evento webhook.test com os mesmos headers da opção que estiver ligada.
Estes headers vão em todo POST e não autenticam a origem. Servem para roteamento e idempotência:
| Header | Valor |
|---|---|
Content-Type | application/json |
User-Agent | Garagem-Webhook/1.0 |
x-tenant-id | UUID do tenant |
x-event-id | Id estável do evento. O mesmo valor é reenviado em cada retentativa |
x-event-name | Nome do evento, por exemplo deal.created |
eventId no body é o mesmo valor de x-event-id. Processe cada id uma vez.
Quando o fato aconteceu: occurredAt
Seção intitulada “Quando o fato aconteceu: occurredAt”Todo evento traz dois horários no body:
| Campo | O que é |
|---|---|
occurredAt | Quando o fato aconteceu no Garagem (ISO-8601, UTC). É fixado quando o evento entra na fila e não muda nas retentativas. |
timestamp | Quando esta tentativa de POST saiu. Muda a cada retentativa. |
Use occurredAt para ordenar eventos e para decidir se um evento é mais novo que o estado que você já tem. A entrega é pelo menos uma vez e pode atrasar (fila, retentativas), então não use timestamp nem o horário de chegada para isso. Para eventos novos, eventId é o id do evento no Garagem (UUID) e é o mesmo em todas as retentativas. Eventos que já estavam na fila antes deste campo existir chegam com occurredAt igual ao horário em que entraram na fila.
Gerar um novo segredo invalida o anterior na hora. Uma retentativa depois da troca sai assinada com o segredo novo, porque cada POST lê a configuração atual. Desligar as duas opções apaga o segredo. Atualizar só a URL, sem mexer nos interruptores, mantém o segredo.
HMAC-SHA256
Seção intitulada “HMAC-SHA256”O segredo não trafega. A assinatura cobre o instante e os bytes exatos do body, então um POST antigo ou um JSON alterado no meio do caminho não confere.
x-webhook-timestampé o tempo Unix em segundos, em decimal, sem aspas no header.- A string canônica é
{timestamp}.{rawBody}: um ponto entre os dois, sem espaço, sem quebra de linha.rawBodysão os bytes exatos do POST, antes deJSON.parsee antes de serializar de novo. Reformatar o JSON (espaços, ordem de chaves) muda a assinatura. x-webhook-signatureésha256=seguido do HMAC-SHA256 dessa string, em hexadecimal minúsculo. A chave do HMAC é o segredo.- Rejeite timestamps com mais de 5 minutos de diferença do relógio do seu servidor (para os dois lados: futuro e passado).
- Compare a assinatura com uma função de tempo constante (
timingSafeEqual,hmac.compare_digest). Uma comparação com===vaza informação pelo tempo de resposta.
Exemplo do que é assinado, com o body exatamente como chegou:
1705315800.{"eventId":"...","eventName":"deal.created","occurredAt":"2024-01-15T10:29:58.120Z","timestamp":"2024-01-15T10:30:00.000Z","tenantId":"...","data":{...}}O campo do payload é data (não eventData). O timestamp de dentro do JSON é ISO-8601 e não entra no HMAC sozinho: entra só como parte dos bytes do body. O valor que você compara com o relógio é o header x-webhook-timestamp.
Token Authorization (Bearer)
Seção intitulada “Token Authorization (Bearer)”O mesmo segredo vai neste header, com o esquema Bearer e um espaço:
Authorization: Bearer <segredo>Compare o token com igualdade de tempo constante. Não use o prefixo Bearer na comparação: só o segredo.
Esse modo existe para um gateway, WAF ou proxy que só sabe exigir um header Authorization fixo. O segredo aparece no header e pode cair em log de acesso, de proxy ou de APM. Prefira HMAC quando o seu processo consegue ler o body cru. Com os dois ligados, o Bearer passa no gateway e o HMAC prova que o body não foi alterado.
Exemplo de verificação (Node.js)
Seção intitulada “Exemplo de verificação (Node.js)”Guarde o body cru no framework (no Express, express.raw({ type: "application/json" }), ou um verify que copia req.rawBody antes do express.json()). JSON.stringify(req.body) não reproduz os bytes originais.
const crypto = require("crypto");
const MAX_AGE_SECONDS = 300;
function verifyGaragemHmac(rawBody, headers, secret) { const timestamp = headers["x-webhook-timestamp"]; const signature = headers["x-webhook-signature"]; if (!timestamp || !signature || !signature.startsWith("sha256=")) { return false; }
const ts = Number(timestamp); if (!Number.isInteger(ts)) return false; if (Math.abs(Date.now() / 1000 - ts) > MAX_AGE_SECONDS) return false;
const expected = "sha256=" + crypto .createHmac("sha256", secret) .update(`${timestamp}.${rawBody}`) .digest("hex");
const a = Buffer.from(signature); const b = Buffer.from(expected); if (a.length !== b.length) return false; return crypto.timingSafeEqual(a, b);}
function verifyGaragemBearer(headers, secret) { const header = headers["authorization"] || ""; const match = /^Bearer\s+(.+)$/i.exec(header); const provided = match ? match[1].trim() : ""; if (!provided) return false;
// SHA-256 dos dois lados iguala o tamanho antes do compare. const a = crypto.createHash("sha256").update(provided).digest(); const b = crypto.createHash("sha256").update(secret).digest(); return crypto.timingSafeEqual(a, b);}Se você ligou só HMAC, chame verifyGaragemHmac. Se ligou só Bearer, chame verifyGaragemBearer. Se ligou os dois, exija os dois.
Exemplo de verificação (Python)
Seção intitulada “Exemplo de verificação (Python)”import hashlibimport hmacimport time
MAX_AGE_SECONDS = 300
def verify_garagem_hmac(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool: if not timestamp or not signature.startswith("sha256="): return False if abs(time.time() - int(timestamp)) > MAX_AGE_SECONDS: return False mac = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256) expected = "sha256=" + mac.hexdigest() return hmac.compare_digest(signature, expected)raw_body é request.data / request.body cru, não o dict já parseado.
Eventos Disponíveis
Seção intitulada “Eventos Disponíveis”Negociações (Deals)
Seção intitulada “Negociações (Deals)”deal.created- Negociação criadadeal.updated- Negociação atualizadadeal.deleted- Negociação deletadadeal.attribution.tracked- Atribuição de marketing atualizada
Anúncios (Listings)
Seção intitulada “Anúncios (Listings)”listing.created- Anúncio criadolisting.updated- Anúncio atualizado (preço, visibilidade, etapa e o restante da linha)listing.workflow.transitioned- Etapa ou status do anúncio mudoulisting.deleted- Anúncio deletadolisting.attribution.tracked- Atribuição de marketing atualizada
Tarefas (Tasks)
Seção intitulada “Tarefas (Tasks)”task.created- Tarefa criadatask.updated- Tarefa atualizadatask.deleted- Tarefa deletada
Imóveis (Properties)
Seção intitulada “Imóveis (Properties)”property.created- Imóvel criadoproperty.updated- Imóvel atualizado
Não há property.deleted. A exclusão que tira o anúncio do ar é listing.deleted.
Preço, publicação e disponibilidade
Seção intitulada “Preço, publicação e disponibilidade”Não existem eventos com nome de preço, publicação, despublicação, privado, vendido ou reservado. Use os eventos acima:
| Fato | Evento | Onde olhar |
|---|---|---|
| Preço | listing.updated | price em data.old e em data.data.listing |
| Etapa ou status | listing.workflow.transitioned | data.data.from e data.data.to, cada um com stage e status. Os valores de status são os do tenant. |
| Publicado no site | listing.updated | stage passa a ser a etapa ativa e visibility é public |
| Despublicado | listing.updated | stage sai da etapa ativa, ou visibility deixa de ser public |
| Privado | listing.updated | visibility = private |
| Anúncio excluído | listing.deleted | data.entityId |
data.data.listing e data.old são a linha do anúncio, não o documento sanitizado da Sites API. Não publique esse payload. Não há objeto changes garantido: compare old com a linha nova. Disponibilidade de unidade de lançamento não tem evento próprio; availableUnits aparece na Sites API na leitura seguinte.
Condomínios (Buildings)
Seção intitulada “Condomínios (Buildings)”building.updated- Condomínio atualizado (dispara a sincronização em rede quando o condomínio está vinculado a uma rede — veja 14 - Condomínios por Canal)
Transações
Seção intitulada “Transações”transaction.created- Transação criadatransaction.updated- Transação atualizadacontract.signed- Contrato assinado por todas as partes no Clicksign. Disparado uma vez por contrato. Quando o contrato é de uma transação, o PDF assinado já está salvo nos documentos da negociação esignedDocumentIdaponta para ele.
Campos de data.data em contract.signed: contractId, transactionId, dealId, listingId, entityType, entityId, documentType, envelopeId, signedDocumentId, occurredAt.
Propostas (Offers)
Seção intitulada “Propostas (Offers)”Um evento por transição da proposta, venha ela do CRM, da Área do Cliente, do link de compartilhamento, do site, de um agente ou da sincronização com outro sistema (integração).
Propostas trazidas pela integração (source = provider_sync) disparam offer.created quando chegam e offer.accepted / offer.rejected quando o status muda no sistema de origem (cancelamento vem como offer.rejected com declineCode = cancelled). Só propostas com movimento recente no sistema de origem (últimos 7 dias) geram evento, então o histórico antigo trazido na primeira importação ou numa ressincronização não dispara nada.
offer.created- Proposta criadaoffer.sent_to_client- Corretor enviou a proposta para o cliente confirmar (status “Aguardando cliente”)offer.client_confirmed- Cliente confirmou as condições na Área do Cliente (com código de verificação por e-mail)offer.client_signed- Cliente assinou a proposta eletronicamente (só quando a imobiliária exige assinatura na proposta)offer.countered- Contraproposta registrada.counterOfferIdé a nova propostaoffer.withdrawn- Cliente retirou a propostaoffer.accepted- Proposta aceita pelo vendedor/gestor.transactionIdé a transação aberta, quando houveroffer.rejected- Proposta recusada.declineCodetraz o motivooffer.expired- Proposta expirou
Campos de data.data em todos os offer.*:
| Campo | Descrição |
|---|---|
offerId | ID da proposta (também em data.entityId, com data.entityType = offer) |
dealId | Negociação da proposta |
listingId | Anúncio da proposta, quando houver |
brokerId | Corretor responsável |
amount | Valor proposto em reais (venda) ou aluguel mensal (locação) |
currency | Sempre BRL |
dealType | sale ou rental |
source | crm, portal, public_link, site, agent, system ou provider_sync |
occurredAt | Quando a transição aconteceu (ISO 8601) |
counterOfferId | Só em offer.countered |
declineCode | Só em offer.rejected / offer.withdrawn |
transactionId | Só em offer.accepted |
documentId | Documento da proposta (PDF) na negociação, em offer.sent_to_client / offer.client_confirmed / offer.client_signed |
{ "eventName": "offer.client_confirmed", "tenantId": "550e8400-e29b-41d4-a716-446655440000", "data": { "entityType": "offer", "entityId": "990e8400-e29b-41d4-a716-446655440009", "data": { "offerId": "990e8400-e29b-41d4-a716-446655440009", "dealId": "660e8400-e29b-41d4-a716-446655440001", "listingId": "aa0e8400-e29b-41d4-a716-44665544000a", "brokerId": "770e8400-e29b-41d4-a716-446655440002", "amount": 850000, "currency": "BRL", "dealType": "sale", "source": "portal", "occurredAt": "2026-09-23T14:05:00.000Z", "documentId": "bb0e8400-e29b-41d4-a716-44665544000b" } }}Distribuição de leads
Seção intitulada “Distribuição de leads”distribution.assigned- Lead atribuído a um corretor por uma filadistribution.accepted- Corretor aceitou o leaddistribution.rejected- Corretor recusou o leaddistribution.transferred- Responsável da negociação mudou.data.data.previousOwnerIdedata.data.newOwnerIddizem de quem para quem.data.data.transferReasonexplica o motivo:member_deactivatedquando o corretor foi desligado e as negociações dele foram passadas para outra pessoa (um evento por negociação).first_contact.skipped- A mensagem automática de primeiro contato não foi enviada para o lead.data.data.reasondiz por quê:opt_out(o contato pediu para não receber mensagens),listing_owner(o telefone é de proprietário de imóvel),won_deal(já tem negócio ganho),open_deal(já tem outra negociação em aberto) ouguard_error(a verificação falhou e, por segurança, nada foi enviado).data.data.destinationdiz para onde o lead foi:keep_with_broker,other_queueouhold.data.data.matchedtraz só ids, nunca telefones.
Mensagens de Canal
Seção intitulada “Mensagens de Canal”Eventos disparados quando mensagens trafegam pelos canais de comunicação (WhatsApp, WhatsApp Cloud, chat interno). Somente mensagens com privacidade whitelisted disparam eventos de entrada — mensagens pendentes ou bloqueadas são totalmente ignoradas.
channel.message.received- Mensagem recebida (inbound)channel.message.sent- Mensagem enviada (outbound)
⚠️ Privacidade e conteúdo: O payload desses eventos NUNCA inclui o conteúdo da mensagem (texto), URLs de mídia, nomes de contato ou avatar. Apenas metadados operacionais (ids, tipo, tamanho, timestamp, direção). Se você precisa do texto ou do histórico, busque sob demanda com a API REST de conversas (
GET /api/v1/messaging/conversations/detail) usando uma API key permanente de/desenvolvedor— padrão recomendado: webhook (metadados) → fetch REST (detalhe), sem polling fixo.
Agentes de IA (Assistentes)
Seção intitulada “Agentes de IA (Assistentes)”Eventos do ciclo de vida de agentes e execuções, úteis para auditoria, dashboards externos e integrações com observabilidade.
agent.created- Agente criadoagent.updated- Agente atualizado (configuração salva pelo operador)agent.deleted- Agente removido (soft delete)agent.execution.started- Execução iniciada (agente começou a rodar)agent.execution.completed- Execução concluída com sucessoagent.execution.failed- Execução falhou
⚠️ Privacidade e conteúdo: O payload desses eventos NUNCA inclui o prompt do agente, conteúdo de skills, política de ferramentas, mensagens da conversa, respostas do LLM, ou inputs/outputs de tool calls. Apenas metadados (ids, nomes, status, contadores, durações, tokens).
Estrutura do Payload de Evento
Seção intitulada “Estrutura do Payload de Evento”{ "eventId": "3f0c2a1e-8b7d-4c5e-9a10-2b3c4d5e6f70", "eventName": "deal.created", "occurredAt": "2024-01-15T10:29:58.120Z", "tenantId": "uuid-do-tenant", "timestamp": "2024-01-15T10:30:00Z", "data": { "entityType": "deal", "entityId": "uuid-do-deal", "userId": "uuid-do-usuario", "data": { "entity": { "id": "uuid-do-deal", "title": "Lead do Site - João Silva", "dealType": "rental", "stage": "lead", "status": "open", "currency": "BRL", "amount": "3500.00", "createdAt": "2024-01-15T10:30:00Z" } }, "old": null, "changes": null }}Exemplo de Evento: Deal Criado
Seção intitulada “Exemplo de Evento: Deal Criado”{ "eventId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "eventName": "deal.created", "occurredAt": "2024-01-15T10:29:59.870Z", "tenantId": "550e8400-e29b-41d4-a716-446655440000", "timestamp": "2024-01-15T10:30:00Z", "data": { "entityType": "deal", "entityId": "660e8400-e29b-41d4-a716-446655440001", "userId": "770e8400-e29b-41d4-a716-446655440002", "data": { "entity": { "id": "660e8400-e29b-41d4-a716-446655440001", "title": "Lead do Google Ads", "dealType": "rental", "stage": "lead", "status": "open", "currency": "BRL", "marketingChannelInstanceId": "880e8400-e29b-41d4-a716-446655440003", "customData": { "maxPrice": 4000, "locations": ["Zona Sul"], "utm_source": "google" }, "contacts": [ { "name": "João Silva", "phone": "+5511999999999", "role": "buyer" } ], "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-01-15T10:30:00Z" } } }}Exemplo de Evento: Listing Atualizado
Seção intitulada “Exemplo de Evento: Listing Atualizado”{ "eventId": "0b1c2d3e-4f50-4a61-8b72-000000000001", "eventName": "listing.updated", "occurredAt": "2024-01-15T10:31:00.000Z", "tenantId": "550e8400-e29b-41d4-a716-446655440000", "timestamp": "2024-01-15T10:31:40Z", "data": { "entityType": "listing", "entityId": "990e8400-e29b-41d4-a716-446655440004", "userId": "770e8400-e29b-41d4-a716-446655440002", "data": { "listing": { "id": "990e8400-e29b-41d4-a716-446655440004", "title": "Apartamento 2 quartos para alugar", "type": "rental", "price": "3500.00", "currency": "BRL", "stage": "active", "visibility": "public", "updatedAt": "2024-01-15T10:31:40Z" } }, "old": { "id": "990e8400-e29b-41d4-a716-446655440004", "price": "3200.00", "stage": "draft", "visibility": "public" } }}Exemplo de Evento: Mensagem Recebida
Seção intitulada “Exemplo de Evento: Mensagem Recebida”{ "eventId": "0b1c2d3e-4f50-4a61-8b72-000000000002", "eventName": "channel.message.received", "occurredAt": "2024-01-15T10:32:00.000Z", "timestamp": "2024-01-15T10:32:00Z", "tenantId": "550e8400-e29b-41d4-a716-446655440000", "data": { "entityType": "channel_message", "entityId": "wamid.HBgMNTUx...", "userId": null, "data": { "channelId": "aa0e8400-e29b-41d4-a716-446655440010", "channelType": "whatsapp_cloud", "chatId": "+5511999999999", "messageId": "wamid.HBgMNTUx...", "messageType": "text", "direction": "inbound", "sessionName": "cloud:whatsapp:123456789", "timestamp": "2024-01-15T10:32:00Z", "textLength": 47, "hasMedia": false, "mediaType": null } }}Exemplo de Evento: Mensagem Enviada
Seção intitulada “Exemplo de Evento: Mensagem Enviada”{ "eventId": "0b1c2d3e-4f50-4a61-8b72-000000000003", "eventName": "channel.message.sent", "occurredAt": "2024-01-15T10:33:00.000Z", "timestamp": "2024-01-15T10:32:05Z", "tenantId": "550e8400-e29b-41d4-a716-446655440000", "data": { "entityType": "channel_message", "entityId": "msg_01HXYZ...", "userId": null, "data": { "channelId": "aa0e8400-e29b-41d4-a716-446655440010", "channelType": "whatsapp_cloud", "chatId": "+5511999999999", "messageId": "msg_01HXYZ...", "messageType": "image", "direction": "outbound", "sessionName": "cloud:whatsapp:123456789", "timestamp": "2024-01-15T10:32:05Z", "textLength": 0, "hasMedia": true, "mediaType": "image/jpeg" } }}Exemplo de Evento: Agente Atualizado
Seção intitulada “Exemplo de Evento: Agente Atualizado”Disparado quando o operador salva alterações em um assistente (prompt, status, comportamento, skills, etc.). changedFields lista apenas os nomes das colunas que mudaram — valores sensíveis nunca são enviados.
{ "eventId": "0b1c2d3e-4f50-4a61-8b72-000000000004", "eventName": "agent.updated", "occurredAt": "2024-01-15T10:34:00.000Z", "timestamp": "2024-01-15T10:33:00Z", "tenantId": "550e8400-e29b-41d4-a716-446655440000", "data": { "entityType": "agent", "entityId": "bb0e8400-e29b-41d4-a716-446655440020", "userId": "770e8400-e29b-41d4-a716-446655440002", "data": { "entity": { "id": "bb0e8400-e29b-41d4-a716-446655440020", "name": "Qualificador de Leads", "description": "Qualifica leads recebidos via WhatsApp", "status": "active", "executionMode": "conversational", "model": "gpt-5.4", "audience": "both", "templateId": null, "createdAt": "2024-01-10T08:00:00Z", "updatedAt": "2024-01-15T10:33:00Z" }, "changedFields": ["status", "promptConfig", "behaviorConfig"] } }}Exemplo de Evento: Execução de Agente Concluída
Seção intitulada “Exemplo de Evento: Execução de Agente Concluída”{ "eventId": "0b1c2d3e-4f50-4a61-8b72-000000000005", "eventName": "agent.execution.completed", "occurredAt": "2024-01-15T10:35:00.000Z", "timestamp": "2024-01-15T10:33:45Z", "tenantId": "550e8400-e29b-41d4-a716-446655440000", "data": { "entityType": "agent_execution", "entityId": "cc0e8400-e29b-41d4-a716-446655440030", "userId": "770e8400-e29b-41d4-a716-446655440002", "data": { "executionId": "cc0e8400-e29b-41d4-a716-446655440030", "agentId": "bb0e8400-e29b-41d4-a716-446655440020", "agentName": "Qualificador de Leads", "triggerType": "message_received", "triggerId": "dd0e8400-e29b-41d4-a716-446655440040", "channelId": "aa0e8400-e29b-41d4-a716-446655440010", "chatId": "+5511999999999", "startedAt": "2024-01-15T10:33:40Z", "completedAt": "2024-01-15T10:33:45Z", "durationMs": 4820, "status": "completed", "tokenUsage": 1240, "toolCount": 3, "toolNames": ["search_properties", "update_lead_profile", "send_message"], "outcome": null } }}Exemplo de Evento: Execução de Agente Falhou
Seção intitulada “Exemplo de Evento: Execução de Agente Falhou”{ "eventId": "0b1c2d3e-4f50-4a61-8b72-000000000006", "eventName": "agent.execution.failed", "occurredAt": "2024-01-15T10:36:00.000Z", "timestamp": "2024-01-15T10:34:10Z", "tenantId": "550e8400-e29b-41d4-a716-446655440000", "data": { "entityType": "agent_execution", "entityId": "cc0e8400-e29b-41d4-a716-446655440031", "userId": "770e8400-e29b-41d4-a716-446655440002", "data": { "executionId": "cc0e8400-e29b-41d4-a716-446655440031", "agentId": "bb0e8400-e29b-41d4-a716-446655440020", "agentName": "Qualificador de Leads", "triggerType": "message_received", "triggerId": "dd0e8400-e29b-41d4-a716-446655440040", "channelId": "aa0e8400-e29b-41d4-a716-446655440010", "chatId": "+5511999999999", "startedAt": "2024-01-15T10:34:05Z", "completedAt": "2024-01-15T10:34:10Z", "durationMs": 5100, "status": "failed", "errorMessage": "Execution limit reached" } }}Configurar Webhook para Eventos
Seção intitulada “Configurar Webhook para Eventos”Para configurar webhooks de eventos, você precisa:
- Acessar Configurações → Webhooks na sua conta
- Criar um novo webhook com a URL do seu endpoint
- Selecionar os eventos que deseja receber
- Configurar a autenticação (se necessário)
O Garagem enviará um POST para sua URL sempre que um evento selecionado ocorrer.
💳 Webhooks de Pagamento
Seção intitulada “💳 Webhooks de Pagamento”O Garagem recebe webhooks de provedores de pagamento para notificações sobre transações financeiras.
Tipos de Eventos
Seção intitulada “Tipos de Eventos”- Boleto - Boleto pago/cancelado/expirado
- PIX - Pagamento PIX realizado/cancelado/expirado
- Transferência - Transferência realizada/falhou/processando
Exemplo de Payload: Boleto Pago
Seção intitulada “Exemplo de Payload: Boleto Pago”{ "event": { "log": { "type": "boleto", "boleto": { "id": "123456789", "status": "paid", "amount": 10000, "taxId": "12345678900", "name": "João Silva", "due": "2024-01-20", "paid": "2024-01-15T10:30:00Z" } } }}Exemplo de Payload: PIX Pago
Seção intitulada “Exemplo de Payload: PIX Pago”{ "event": { "log": { "type": "invoice", "invoice": { "id": "987654321", "status": "paid", "amount": 5000, "taxId": "98765432100", "name": "Maria Santos", "due": "2024-01-18", "paid": "2024-01-15T11:00:00Z" } } }}📝 Webhooks de Assinatura Digital
Seção intitulada “📝 Webhooks de Assinatura Digital”O Garagem recebe webhooks de provedores de assinatura digital para notificações sobre documentos e assinaturas.
Tipos de Eventos
Seção intitulada “Tipos de Eventos”upload- Documento enviadoadd_signer- Signatário adicionadosign- Assinatura realizadadocument_closed- Documento concluído (todos assinaram)refusal- Recusa de assinaturacancel- Cancelamentodeadline- Prazo expirado
Exemplo de Payload: Assinatura Realizada
Seção intitulada “Exemplo de Payload: Assinatura Realizada”{ "event": "sign", "data": { "document": { "key": "document-key-123", "name": "Contrato de Locação" }, "signer": { "key": "signer-key-456", "name": "João Silva" }, "envelope": { "key": "envelope-key-789" } }, "timestamp": "2024-01-15T10:30:00Z"}Exemplo de Payload: Documento Concluído
Seção intitulada “Exemplo de Payload: Documento Concluído”{ "event": "document_closed", "data": { "document": { "key": "document-key-123", "name": "Contrato de Locação" }, "envelope": { "key": "envelope-key-789" } }, "timestamp": "2024-01-15T10:35:00Z"}🔒 Entrega dos eventos do sistema
Seção intitulada “🔒 Entrega dos eventos do sistema”Cada POST leva um evento. Os headers sempre presentes (Content-Type, x-tenant-id, x-event-id, x-event-name, User-Agent) e a autenticação opcional (HMAC / Bearer) estão em Autenticação dos eventos de saída. Enquanto nenhuma opção de autenticação estiver ligada, os headers de assinatura e de Bearer não são enviados.
eventId tem a forma {tenantId}_{eventName}_{epochMs}_{sufixo}. É gerado uma vez e repetido em todas as tentativas. Processe cada eventId uma vez.
timestamp no corpo é o horário do envio, em ISO-8601, e muda a cada tentativa. Não é o valor do header x-webhook-timestamp (esse só existe quando HMAC está ligado) e, sozinho, não autentica a origem.
Timeout de 30 segundos. Resposta que não é 2xx, ou falha de rede, mantém o evento na fila. A nova tentativa sai no ciclo seguinte (cerca de um minuto), não em backoff imediato. A fila do tenant é FIFO: um evento que falha segura os seguintes. Depois de 8 falhas o evento é descartado. Se o endpoint ficar indisponível, os eventos permanecem na fila até sucesso ou até a oitava falha.
Resumo do que validar no seu endpoint:
- HMAC — header
x-webhook-signaturesobre{timestamp}.{rawBody}, com o body cru. Rejeite timestamps fora de uma janela de 5 minutos. - Bearer — header
Authorization: Bearer <segredo>, quando essa opção estiver ligada. O segredo pode aparecer em logs; HMAC não envia o segredo. - Idempotência — processe cada
eventId/x-event-iduma vez. O Garagem reenvia o mesmo id quando o POST falha. - Origem —
User-Agent: Garagem-Webhook/1.0ex-tenant-ididentificam o evento, mas não provam que ele saiu do Garagem. Não há IPs de saída fixos.
Webhooks de entrada (você chamando o Garagem) usam o segredo do canal, descrito em Autenticação no início desta página: Bearer ou x-hmac-sha256 / x-signature sobre o body cru. Esse contrato é o inverso e não usa x-webhook-signature.
📋 Resumo
Seção intitulada “📋 Resumo”| Tipo | Endpoint | Uso |
|---|---|---|
| Canais (Inbound) | /api/webhooks/channels/{pathToken} | Receber leads de canais externos |
| Canais (Outbound) | /api/webhooks/channels/{pathToken}/listings | Expor imóveis filtrados a sistemas externos (13 - Listings por Canal) |
| Condomínios (Inbound em lote) | /api/webhooks/channels/{pathToken}/buildings/import | Importar condomínios/edifícios em lote (14 - Condomínios por Canal) |
| Condomínios (Outbound) | /api/webhooks/channels/{pathToken}/buildings | Expor condomínios/edifícios a sistemas externos (14 - Condomínios por Canal) |
| Eventos do Sistema | Configurável | Receber notificações sobre eventos internos |
| Pagamento | Configurável | Notificações de pagamento |
| Assinatura Digital | Configurável | Notificações de assinatura |
🔗 Próximos Passos
Seção intitulada “🔗 Próximos Passos”- Configure seus webhooks nas Configurações → Webhooks
- Teste seus endpoints usando ferramentas como ngrok para desenvolvimento local
- Consulte a Referência da API para detalhes completos dos endpoints