API & MCP
Webhooks de saída
Em vez de o seu sistema perguntar de tempos em tempos se apareceu algo novo, a ionluz avisa. Cadastre uma URL em API e cobertura, guarde o segredo que aparece na hora e passe a receber os eventos da sua conta.
| Evento | Quando dispara |
|---|---|
lead.criado | Uma proposta foi gravada na sua conta: pela página pública de captação, por POST /v1/quote com gravação ou pelo MCP com persist. |
proposta.aberta | O cliente final abriu a proposta pela primeira vez. Sai uma vez por proposta, mesmo gatilho do e-mail de aviso. |
proposta.aceita | O cliente final aceitou a proposta na página dele, dentro da validade. Sai uma vez por proposta, mesmo gatilho do e-mail de aceite. |
ping | Você clicou em Testar no painel. Use para validar a sua verificação de assinatura antes de contar com os eventos reais. |
O que chega
POST https://seucrm.com.br/hooks/ionluz
X-Ionluz-Event: lead.criado
X-Ionluz-Delivery: evt_3f1a9c72b0d84e5f16
X-Ionluz-Signature: t=1789012345,v1=6b1f...c904
Content-Type: application/json
{
"id": "evt_3f1a9c72b0d84e5f16",
"evento": "lead.criado",
"criadoEm": "2026-09-12T14:05:00.000Z",
"data": {
"proposalId": "clx8h2k9a0001abcd",
"origem": "api",
"criadaEm": "2026-09-12T14:05:00.000Z",
"potenciaKwp": 5.4,
"valorFinal": 28500,
"paybackAnos": 6.3,
"consumoKwhMes": 600,
"cliente": { "nome": "Ana", "email": "ana@ex.com", "telefone": "15999990000",
"cidade": "Sorocaba", "uf": "SP" },
"links": { "proposta": "https://app.ionluz.com.br/p/...", "pdf": "https://app.ionluz.com.br/api/pub/.../pdf?t=..." }
}
}Conferindo a assinatura
O header X-Ionluz-Signature vem no formato t=<unix>,v1=<hmac-sha256-hex>. O HMAC é calculado com o seu segredo sobre a string <t>.<corpo bruto>. Compare em tempo constante e recuse timestamp com mais de 5 minutos, isso fecha a porta para reenvio de uma entrega capturada.
// Express: guarde o corpo BRUTO, a assinatura é sobre os bytes recebidos
app.post("/hooks/ionluz", express.raw({ type: "application/json" }), (req, res) => {
const bruto = req.body.toString("utf8");
const partes = Object.fromEntries(
req.get("X-Ionluz-Signature").split(",").map(p => p.split("="))
);
const esperado = crypto.createHmac("sha256", process.env.IONLUZ_WEBHOOK_SECRET)
.update(partes.t + "." + bruto).digest("hex");
const assinaturaOk =
esperado.length === partes.v1.length &&
crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(partes.v1));
const recente = Math.abs(Date.now() / 1000 - Number(partes.t)) < 300;
if (!assinaturaOk || !recente) return res.sendStatus(401);
res.sendStatus(200); // responda primeiro
processar(JSON.parse(bruto)); // trabalhe depois, fora do ciclo da entrega
});Entrega
- Timeout de 8 segundos e uma retentativa, em falha de rede,
5xxou429. Qualquer outro4xxencerra a entrega, porque repetir não mudaria o resultado. - A retentativa reenvia o mesmo corpo e a mesma assinatura. Deduplique pelo
X-Ionluz-Delivery, que é oiddo envelope. - Responda
2xxprimeiro e processe depois. Enquanto o seu servidor pensa, o relógio dos 8 segundos corre. - Sem fila de reprocessamento: o que acontecer com o webhook removido ou fora do ar por muito tempo não é reenviado depois. Para reconciliar, use
GET /v1/proposals. - Só
httpse host público. Redirect não é seguido. - O painel mostra o status da última entrega. Rotacionar o segredo vale na entrega seguinte.
- Emitir um evento nunca atrasa nem derruba a resposta ao seu cliente: a cotação volta antes, o aviso sai depois.
API v1 · ionluz, orçamentos solares. Dúvidas? Cite o
requestId ao escrever para suporte@ionluz.com.br.