Pular para o conteúdo

Webhooks e Eventos

Esta documentação explica como receber notificações em tempo real sobre eventos que acontecem no Garagem através de webhooks.


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 pathToken também entrega imóveis filtrados em JSON via Listings por Canal (Feed JSON) — autenticado por Bearer token do canal, com suporte a slots/destaque.

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

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 (ou x-signature) com o HMAC-SHA256 hexadecimal do body cru. Prefixo sha256= é 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

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: listing

Ou via body:

{
"destination": "listing"
}

Nota: O destino precisa estar habilitado na configuração do canal. Se o canal permitir apenas deal, enviar listing retornará erro 403.

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
}

O webhook aceita os campos abaixo no body JSON. Todos são opcionais — envie apenas o que tiver disponível.

CampoAliases aceitosDescrição
contactNamename, ownerNameNome do contato
contactEmailemail, ownerEmailE-mail do contato
contactPhonephone, ownerPhoneTelefone (formato E.164: +5511999999999)
cpfownerCpfCPF (opcional)
CampoDescrição
listingIdID (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
listingCodeCódigo de referência do anúncio (ex: 423699, AP-001). Alternativa ao UUID — resolve o imóvel pelo código do catálogo
propertyIdAlternativa ao listingId — UUID do imóvel/anúncio, ou código do catálogo se não for UUID
clientListingIdAlias 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.), envie listingCode — 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.

CampoValoresDescrição
businessTypesale, rent, rental, bothTipo de negócio. Se não enviado, assume sale

O businessType define o tipo da negociação criada:

  • sale → Negociação de venda
  • rent ou rental → Negociação de locação
  • both → Venda e locação

Se você não tiver o listingId, pode enviar os dados diretamente:

CampoTipoDescrição
propertyTypestringapartment, house, land, commercial, etc.
bedroomsnumberQuartos
bathroomsnumberBanheiros
suitesnumberSuítes
parkingSpacesnumberVagas
totalAreanumberÁrea total (m²)
usableAreanumberÁrea útil (m²)
pricenumberValor do imóvel
condominiumFeenumberValor do condomínio
{
"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"
}
}
CampoDescrição
messageMensagem do lead (ex: “Tenho interesse neste imóvel”)
sourceOrigem do lead (ex: “Site”, “Facebook Ads”)
campaignNome da campanha
leadTypeTipo do lead: visit, contact (interesse do comprador), seller_lead (captação / anunciar imóvel). Alias legado de captação: property
CampoFormatoDescrição
visitDateYYYY-MM-DDData da visita
visitTimeHH:mmHorário da visita
visitNotesstringObservações sobre a visita

Quando visitDate e visitTime são enviados, o Garagem cria automaticamente uma tarefa de visita vinculada à negociação.

CampoDescrição
utmSourceOrigem (ex: google, facebook)
utmMediumMeio (ex: cpc, social, email)
utmCampaignCampanha
utmTermTermo de busca
utmContentConteúdo do anúncio
referrerURL absoluta de referência (https://www.google.com). Um valor que não é URL é ignorado e não impede o lead.
landingPagePá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.
gclidGoogle Click ID
CampoDescrição
destinationdeal ou listing — controle de destino via body
originLeadIdID único do lead na origem — evita duplicatas. Se enviado duas vezes com o mesmo valor, o segundo retorna a negociação existente (status: "duplicate")
externalIdReferência externa (para rastreabilidade)

O exemplo mais simples — apenas nome, telefone e mensagem:

Janela do terminal
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"
}'

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:

Janela do terminal
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):

Janela do terminal
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.

Especifique businessType para diferenciar leads de venda e locação:

Locação:

Janela do terminal
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:

Janela do terminal
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 o businessType a partir da URL e envie no payload.

Quando você não tem o listingId, envie os dados do imóvel diretamente:

Janela do terminal
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"
}'

Envie visitDate e visitTime para criar automaticamente uma tarefa de visita:

Janela do terminal
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"
}'

Exemplo com todos os campos de rastreamento UTM:

Janela do terminal
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".

Para leads de proprietários que querem anunciar um imóvel, use destination: "listing":

Janela do terminal
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.

Use originLeadId para evitar duplicatas quando o mesmo lead for enviado mais de uma vez:

Janela do terminal
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 originLeadId for enviado novamente para o mesmo canal, o Garagem não cria outra negociação. Com ?sync=true, a resposta inclui o dealId original e status: "duplicate". Sem sync, o padrão continua 202 { "ok": true, "accepted": true } na primeira entrega.


O Garagem emite eventos quando entidades são criadas, atualizadas ou deletadas. Você pode configurar webhooks para receber notificações sobre esses eventos.

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çãoO que ligarHeaders adicionados
HMAC-SHA256Assinar com HMACx-webhook-timestamp, x-webhook-signature
BearerEnviar token AuthorizationAuthorization: Bearer <segredo>
As duasOs dois interruptoresOs três headers, com o mesmo segredo
NenhumaPadrãoNenhum 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:

HeaderValor
Content-Typeapplication/json
User-AgentGaragem-Webhook/1.0
x-tenant-idUUID do tenant
x-event-idId estável do evento. O mesmo valor é reenviado em cada retentativa
x-event-nameNome do evento, por exemplo deal.created

eventId no body é o mesmo valor de x-event-id. Processe cada id uma vez.

Todo evento traz dois horários no body:

CampoO que é
occurredAtQuando o fato aconteceu no Garagem (ISO-8601, UTC). É fixado quando o evento entra na fila e não muda nas retentativas.
timestampQuando 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.

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.

  1. x-webhook-timestamp é o tempo Unix em segundos, em decimal, sem aspas no header.
  2. A string canônica é {timestamp}.{rawBody}: um ponto entre os dois, sem espaço, sem quebra de linha. rawBody são os bytes exatos do POST, antes de JSON.parse e antes de serializar de novo. Reformatar o JSON (espaços, ordem de chaves) muda a assinatura.
  3. x-webhook-signature é sha256= seguido do HMAC-SHA256 dessa string, em hexadecimal minúsculo. A chave do HMAC é o segredo.
  4. Rejeite timestamps com mais de 5 minutos de diferença do relógio do seu servidor (para os dois lados: futuro e passado).
  5. 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.

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.

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.

import hashlib
import hmac
import 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.

  • deal.created - Negociação criada
  • deal.updated - Negociação atualizada
  • deal.deleted - Negociação deletada
  • deal.attribution.tracked - Atribuição de marketing atualizada
  • listing.created - Anúncio criado
  • listing.updated - Anúncio atualizado (preço, visibilidade, etapa e o restante da linha)
  • listing.workflow.transitioned - Etapa ou status do anúncio mudou
  • listing.deleted - Anúncio deletado
  • listing.attribution.tracked - Atribuição de marketing atualizada
  • task.created - Tarefa criada
  • task.updated - Tarefa atualizada
  • task.deleted - Tarefa deletada
  • property.created - Imóvel criado
  • property.updated - Imóvel atualizado

Não há property.deleted. A exclusão que tira o anúncio do ar é listing.deleted.

Não existem eventos com nome de preço, publicação, despublicação, privado, vendido ou reservado. Use os eventos acima:

FatoEventoOnde olhar
Preçolisting.updatedprice em data.old e em data.data.listing
Etapa ou statuslisting.workflow.transitioneddata.data.from e data.data.to, cada um com stage e status. Os valores de status são os do tenant.
Publicado no sitelisting.updatedstage passa a ser a etapa ativa e visibility é public
Despublicadolisting.updatedstage sai da etapa ativa, ou visibility deixa de ser public
Privadolisting.updatedvisibility = private
Anúncio excluídolisting.deleteddata.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.

  • 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)
  • transaction.created - Transação criada
  • transaction.updated - Transação atualizada
  • contract.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 e signedDocumentId aponta para ele.

Campos de data.data em contract.signed: contractId, transactionId, dealId, listingId, entityType, entityId, documentType, envelopeId, signedDocumentId, occurredAt.

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 criada
  • offer.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 proposta
  • offer.withdrawn - Cliente retirou a proposta
  • offer.accepted - Proposta aceita pelo vendedor/gestor. transactionId é a transação aberta, quando houver
  • offer.rejected - Proposta recusada. declineCode traz o motivo
  • offer.expired - Proposta expirou

Campos de data.data em todos os offer.*:

CampoDescrição
offerIdID da proposta (também em data.entityId, com data.entityType = offer)
dealIdNegociação da proposta
listingIdAnúncio da proposta, quando houver
brokerIdCorretor responsável
amountValor proposto em reais (venda) ou aluguel mensal (locação)
currencySempre BRL
dealTypesale ou rental
sourcecrm, portal, public_link, site, agent, system ou provider_sync
occurredAtQuando a transição aconteceu (ISO 8601)
counterOfferIdSó em offer.countered
declineCodeSó em offer.rejected / offer.withdrawn
transactionIdSó em offer.accepted
documentIdDocumento 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"
}
}
}
  • distribution.assigned - Lead atribuído a um corretor por uma fila
  • distribution.accepted - Corretor aceitou o lead
  • distribution.rejected - Corretor recusou o lead
  • distribution.transferred - Responsável da negociação mudou. data.data.previousOwnerId e data.data.newOwnerId dizem de quem para quem. data.data.transferReason explica o motivo: member_deactivated quando 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.reason diz 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) ou guard_error (a verificação falhou e, por segurança, nada foi enviado). data.data.destination diz para onde o lead foi: keep_with_broker, other_queue ou hold. data.data.matched traz só ids, nunca telefones.

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.

Eventos do ciclo de vida de agentes e execuções, úteis para auditoria, dashboards externos e integrações com observabilidade.

  • agent.created - Agente criado
  • agent.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 sucesso
  • agent.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).

{
"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
}
}
{
"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",
"email": "[email protected]",
"phone": "+5511999999999",
"role": "buyer"
}
],
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
}
}
}
{
"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"
}
}
}
{
"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
}
}
}
{
"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"
}
}
}

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
}
}
}
{
"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"
}
}
}

Para configurar webhooks de eventos, você precisa:

  1. Acessar Configurações → Webhooks na sua conta
  2. Criar um novo webhook com a URL do seu endpoint
  3. Selecionar os eventos que deseja receber
  4. Configurar a autenticação (se necessário)

O Garagem enviará um POST para sua URL sempre que um evento selecionado ocorrer.


O Garagem recebe webhooks de provedores de pagamento para notificações sobre transações financeiras.

  • Boleto - Boleto pago/cancelado/expirado
  • PIX - Pagamento PIX realizado/cancelado/expirado
  • Transferência - Transferência realizada/falhou/processando
{
"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"
}
}
}
}
{
"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"
}
}
}
}

O Garagem recebe webhooks de provedores de assinatura digital para notificações sobre documentos e assinaturas.

  • upload - Documento enviado
  • add_signer - Signatário adicionado
  • sign - Assinatura realizada
  • document_closed - Documento concluído (todos assinaram)
  • refusal - Recusa de assinatura
  • cancel - Cancelamento
  • deadline - Prazo expirado
{
"event": "sign",
"data": {
"document": {
"key": "document-key-123",
"name": "Contrato de Locação"
},
"signer": {
"key": "signer-key-456",
"email": "[email protected]",
"name": "João Silva"
},
"envelope": {
"key": "envelope-key-789"
}
},
"timestamp": "2024-01-15T10:30:00Z"
}
{
"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"
}

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:

  1. HMAC — header x-webhook-signature sobre {timestamp}.{rawBody}, com o body cru. Rejeite timestamps fora de uma janela de 5 minutos.
  2. Bearer — header Authorization: Bearer <segredo>, quando essa opção estiver ligada. O segredo pode aparecer em logs; HMAC não envia o segredo.
  3. Idempotência — processe cada eventId / x-event-id uma vez. O Garagem reenvia o mesmo id quando o POST falha.
  4. Origem — User-Agent: Garagem-Webhook/1.0 e x-tenant-id identificam 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.


TipoEndpointUso
Canais (Inbound)/api/webhooks/channels/{pathToken}Receber leads de canais externos
Canais (Outbound)/api/webhooks/channels/{pathToken}/listingsExpor imóveis filtrados a sistemas externos (13 - Listings por Canal)
Condomínios (Inbound em lote)/api/webhooks/channels/{pathToken}/buildings/importImportar condomínios/edifícios em lote (14 - Condomínios por Canal)
Condomínios (Outbound)/api/webhooks/channels/{pathToken}/buildingsExpor condomínios/edifícios a sistemas externos (14 - Condomínios por Canal)
Eventos do SistemaConfigurávelReceber notificações sobre eventos internos
PagamentoConfigurávelNotificações de pagamento
Assinatura DigitalConfigurávelNotificações de assinatura

  • 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