Integrar
Webhooks
Receba no seu servidor um aviso a cada solicitação e a cada resposta do cliente.
Nesta página
Configurar
Em Propostas › Integração, no cartão Webhook (dono e administradores):
- Informe o endereço
https://do seu servidor que vai receber os avisos. É um endereço por empresa, o mesmo nos dois modos. Apagar o endereço desliga o webhook. - Ao salvar o primeiro endereço, a Lafra cria o segredo
whsec_…e o mostra inteiro uma vez só. Guarde no seu servidor (ele confere a assinatura). “Gerar novo segredo” troca o segredo na hora: o anterior para de valer. - Escolha os eventos. Já vêm marcados
solicitacao.criada,proposta.aprovadaeproposta.recusada. - Use “Enviar teste” para conferir. Enviar teste.
O cartão mostra os últimos envios, e cada tentativa aparece na Atividade recente com o código que o seu servidor respondeu.
Eventos
O corpo do envio
Todo envio é um POST com um JSON assim:
{
"id": "a3c5e7f9-1b2d-4e6f-8a0c-2d4f6b8d0e1a",
"tipo": "proposta.aprovada",
"modo": "producao",
"criado_em": "2026-10-29T14:32:07.512Z",
"dados": { ... }
}dados de solicitacao.criada
Os campos que o formulário não pede (ou que a pessoa deixou vazios) vêm null (servicos, como lista vazia). O contato vem em telefone (no formato (21) 90000-1234) ou em email (em minúsculas). Os anexos trazem o nome e o tamanho em bytes; os arquivos ficam na Lafra.
anexos é a lista que o formulário declarou no envio: o evento sai logo que a solicitação é recebida, antes de os arquivos subirem. Os arquivos chegam depois, e só os que chegarem inteiros ficam na solicitação; um arquivo da lista pode não existir na Lafra.{
"codigo": "SL-0007",
"recebida_em": "2026-10-29T14:30:55.104Z",
"nome": "Policarpo Quaresma",
"empresa": "Sítio do Sossego Ltda.",
"telefone": "(21) 90000-1234",
"email": null,
"documento": null,
"endereco": "Estrada do Sossego, 1910, Rio de Janeiro - RJ",
"tipo_obra": "Residencial unifamiliar",
"area_m2": 860.5,
"pavimentos": 2,
"servicos": ["spt", "trado"],
"prazo": "Até dezembro",
"mensagem": "Terreno em aclive, com acesso por estrada de terra.",
"anexos": [{ "nome": "planta-baixa.pdf", "tamanho": 482113 }],
"origem": {
"dominio": "www.sondagens-exemplo.com.br",
"pagina": "/orcamento",
"campanha": "google / cpc"
}
}dados de proposta.vista, proposta.aprovada e proposta.recusada
{
"codigo": "PR-0042",
"titulo": "SPT – Construtora Quincas Borba",
"cliente": "Construtora Quincas Borba",
"status": "aprovada",
"valor_total": 1840000,
"link": "https://lafra.app/p/5f2d8c1e-7a44-4b9e-8e0d-3c6b1a9f2e77",
"quando": "2026-10-29T14:32:07.498Z",
"aceite": { "nome": "Brás Cubas", "cargo": "Diretor de obras" }
}Em webhook.teste, dados é um objeto vazio ({}).
Cabeçalhos
POST /webhooks/lafra HTTP/1.1
Content-Type: application/json
User-Agent: Lafra-Webhook/1.0
Lafra-Evento: proposta.aprovada
Lafra-Entrega: a3c5e7f9-1b2d-4e6f-8a0c-2d4f6b8d0e1a
Lafra-Assinatura: t=1793284327,v1=5d41b3c8e0f2...Conferir a assinatura
Confira a assinatura em todo envio, antes de usar os dados. Assim você sabe que ele veio da Lafra e não foi alterado no caminho.
- Leia
tev1do cabeçalhoLafra-Assinatura. - Recuse se
testiver a mais de 5 minutos do relógio do seu servidor (protege contra o reenvio de um pedido antigo). - Calcule o HMAC-SHA256 de
<t>.<corpo>(o número, um ponto e o corpo exato, byte a byte, como chegou) com o segredo inteiro,whsec_…incluído, como chave. - Compare com
v1em tempo constante (crypto.timingSafeEqual,hmac.compare_digest), nunca com==.
import crypto from "node:crypto";
import express from "express";
const app = express();
// O segredo inteiro, com o "whsec_", numa variável de ambiente.
const SEGREDO = process.env.LAFRA_WEBHOOK_SEGREDO;
// express.raw: a assinatura é do corpo exato, antes de virar objeto.
app.post("/webhooks/lafra", express.raw({ type: "application/json" }), (req, res) => {
const corpo = req.body.toString("utf8");
if (!assinaturaValida(req.get("Lafra-Assinatura"), corpo)) {
return res.sendStatus(400);
}
const evento = JSON.parse(corpo);
// Use evento.id para não tratar o mesmo envio duas vezes.
console.log(evento.tipo, evento.dados);
res.sendStatus(200);
});
function assinaturaValida(cabecalho, corpo) {
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(cabecalho ?? "");
if (!m) return false;
const t = Number(m[1]);
// Recusa assinaturas com mais de 5 minutos (para os dois lados).
if (Math.abs(Math.floor(Date.now() / 1000) - t) > 300) return false;
const esperado = crypto
.createHmac("sha256", SEGREDO)
.update(`${t}.${corpo}`, "utf8")
.digest();
const recebido = Buffer.from(m[2], "hex");
return (
recebido.length === esperado.length &&
crypto.timingSafeEqual(esperado, recebido)
);
}
app.listen(3000);import hashlib
import hmac
import json
import os
import re
import time
from flask import Flask, abort, request
app = Flask(__name__)
# O segredo inteiro, com o "whsec_", numa variável de ambiente.
SEGREDO = os.environ["LAFRA_WEBHOOK_SEGREDO"].encode()
def assinatura_valida(cabecalho: str | None, corpo: bytes) -> bool:
m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", cabecalho or "")
if not m:
return False
t = int(m.group(1))
# Recusa assinaturas com mais de 5 minutos (para os dois lados).
if abs(int(time.time()) - t) > 300:
return False
esperado = hmac.new(SEGREDO, f"{t}.".encode() + corpo, hashlib.sha256).hexdigest()
# compare_digest: comparação em tempo constante.
return hmac.compare_digest(esperado, m.group(2))
@app.post("/webhooks/lafra")
def webhook_da_lafra():
corpo = request.get_data() # o corpo exato, em bytes
if not assinatura_valida(request.headers.get("Lafra-Assinatura"), corpo):
abort(400)
evento = json.loads(corpo)
# Use evento["id"] para não tratar o mesmo envio duas vezes.
print(evento["tipo"], evento["dados"])
return "", 200Respostas e reenvios
Responda com um código 2xx em até 5 segundos: é o que conta como entregue. O conteúdo da resposta é ignorado. Se for demorar, responda primeiro e processe depois.
Qualquer outra coisa (outro código, redirecionamento, falta de resposta em 5 s, erro de conexão) é uma falha, e a Lafra tenta de novo, com o mesmo corpo e o mesmo id:
Depois da 6ª falha o envio é dado como perdido. A espera é o mínimo: as novas tentativas saem quando a Lafra processa a fila (a cada evento, a cada pedido à API e quando alguém abre a Integração), então podem atrasar um pouco além do tempo da tabela.
id e ignore os repetidos.Regras do endereço
Para proteger a sua rede e a da Lafra, o endereço do webhook tem regras:
- Só
https://, na porta 443 (padrão), com até 500 letras, sem espaços e sem usuário ou senha no endereço. - Nada da rede interna:
localhost, nomes terminados em.local,.internal,.localhost,.lan,.home,.corp…, e IPs privados, locais ou reservados (também IPv4 dentro de IPv6). - O nome é conferido no DNS ao salvar e de novo a cada envio.
- A Lafra não segue redirecionamentos: um 301 ou 302 conta como falha.
- 5 segundos no máximo por tentativa.
Para testar no seu computador, use um túnel com endereço https público.
Enviar teste
“Enviar teste” manda o evento webhook.teste uma vez só (sem novas tentativas) e mostra a resposta do seu servidor. O modo do corpo é o do modo aberto na Integração. Vale até 10 vezes por hora por empresa.
Os pedidos do formulário no modo Teste não geram solicitacao.criada: para ver um evento de verdade, use Produção num domínio autorizado.