Buscar K
Aparência
Aparência
O receptor do webhook é qualquer endpoint HTTPS público que aceite um POST JSON e responda 2xx em até 10 segundos. Dá para ver o POST chegando num serviço de inspeção antes de ter o seu; o receptor de verdade precisa ainda autenticar a origem e descartar repetições.
A URL do serviço de inspeção fica visível para quem a tiver. Troque pela URL definitiva depois do teste.
Para testar o seu receptor antes de ligar no Lumo, mande um corpo de exemplo com curl:
curl -X POST https://seu-endpoint.exemplo.com/lumo/alertas \
-H 'Content-Type: application/json' \
-H 'X-Auth-Token: seu-token-secreto' \
-d '{"alert":{"id":412,"nome":"Teste","type":"trigger","app_id":null},
"user":{"id":87,"nome":"Maria","email":"maria@empresa.com.br"},
"tenantId":23,"timestamp":"2026-10-30T10:30:00.512Z",
"content":[{"id":"x","type":"text","nome":"Resumo","generated":{"text":"Olá"}}]}'O corpo completo está em payload.
O Lumo não assina o POST. Para o receptor recusar quem não é o Lumo, use um dos dois, ou os dois juntos:
X-Auth-Token com um valor longo e aleatório, e recuse o POST sem ele./lumo/alertas/7f3a9b2e4c1d). Quem não conhece o caminho recebe 404.Gere o segredo com openssl rand -hex 24 e troque se ele vazar.
O Lumo repete o POST até 8 vezes quando não recebe 2xx a tempo. Se o receptor processou e a resposta se perdeu, o mesmo POST chega de novo. Não existe chave de idempotência no cabeçalho; monte a sua com três campos do corpo:
<alert.id>:<user.id>:<timestamp>O timestamp é fixado quando o POST é criado e não muda entre tentativas. Cada destinatário tem o próprio POST, com timestamp próprio; o user.id entra na chave para que dois POSTs de pessoas diferentes nunca se confundam, mesmo criados no mesmo milissegundo.
Guarde as chaves já vistas por pelo menos 10 minutos, tempo de sobra para as 8 tentativas.
O tempo limite é de 10 segundos por tentativa. Responda 200 logo depois de validar e enfileirar, e faça o trabalho pesado (baixar arquivos, chamar outros sistemas) depois. Responder 4xx também gera nova tentativa; para recusar um POST de vez, responda 2xx e descarte.
POST e um path secreto.{{$json.alert.id}} ou {{$json.alert.nome}}, já que a URL é a mesma para todos os alertas do Tenant.import express from "express";
const app = express();
app.use(express.json({ limit: "5mb" }));
const TOKEN = process.env.LUMO_WEBHOOK_TOKEN;
const vistos = new Map(); // chave -> expira em (ms)
app.post("/lumo/alertas", (req, res) => {
if (req.get("X-Auth-Token") !== TOKEN) return res.sendStatus(401);
const { alert, user, timestamp, content } = req.body;
const chave = `${alert.id}:${user.id}:${timestamp}`;
const agora = Date.now();
for (const [k, exp] of vistos) if (exp < agora) vistos.delete(k);
if (vistos.has(chave)) return res.sendStatus(200);
vistos.set(chave, agora + 10 * 60 * 1000);
res.sendStatus(200);
setImmediate(() => processar(alert, user, content));
});
function processar(alert, user, content) {
for (const bloco of content) {
const g = bloco.generated;
if (bloco.type === "text" || bloco.type === "ai") console.log(bloco.nome, g.text);
if (bloco.type === "report") console.log(bloco.nome, g.pdf, g.xlsx);
if (bloco.type === "chart" && g.kind === "dashboard") console.log("dashboard", g.url);
else if (bloco.type === "chart" && (g.chart || g.error)) console.log("gráfico", g.chart ?? g.error);
else if (bloco.type === "chart") console.log("bloco sem conteúdo (falhou)");
}
}
app.listen(3000);Com mais de uma instância do receptor, troque o Map por um armazenamento compartilhado, como Redis com SET chave 1 NX EX 600.
import os, time
from fastapi import FastAPI, Request, BackgroundTasks, HTTPException
app = FastAPI()
TOKEN = os.environ["LUMO_WEBHOOK_TOKEN"]
vistos: dict[str, float] = {}
@app.post("/lumo/alertas")
async def receber(req: Request, tarefas: BackgroundTasks):
if req.headers.get("x-auth-token") != TOKEN:
raise HTTPException(status_code=401)
corpo = await req.json()
chave = f'{corpo["alert"]["id"]}:{corpo["user"]["id"]}:{corpo["timestamp"]}'
agora = time.time()
for k in [k for k, exp in vistos.items() if exp < agora]:
del vistos[k]
if chave in vistos:
return {"ok": True}
vistos[chave] = agora + 600
tarefas.add_task(processar, corpo)
return {"ok": True}
def processar(corpo: dict):
for bloco in corpo["content"]:
print(bloco["type"], bloco["nome"], bloco["generated"])PDFs, planilhas e imagens chegam como links. Baixe o arquivo durante o processamento e guarde no seu armazenamento, em vez de gravar o link do Lumo como endereço permanente.
| Sintoma | Causa provável |
|---|---|
| Nenhum POST chega | Canal Webhook desmarcado no alerta, ou o canal não está liberado em Canais de Alerta |
Falhou com Invalid webhook URL | URL vazia no Tenant e no Cliente |
| O Tenant não salva a URL | A URL quebra uma das regras: https, porta 443, sem usuário e senha, endereço público |
| Falhou depois de 4 a 6 minutos | O receptor demora mais de 10 segundos, responde fora de 2xx ou tem certificado TLS inválido |
| O mesmo conteúdo chega duas vezes | A resposta 2xx chegou tarde. Descarte pela chave de repetição |
Um bloco chega com error ou com generated vazio | A geração daquele bloco falhou (Gráfico traz error; Dashboard e Relatório chegam {}). Os outros vêm normais |
| Cabeçalho customizado não chega | Nome reservado ou inválido; o editor de cabeçalhos aponta o erro |
No Histórico de Envio do alerta, cada destinatário mostra o canal Webhook como Na fila, Enviado ou Falhou. Na falha, aparece o erro da última tentativa, como HTTP 500: Internal Server Error ou Request timeout after 10000ms; a resposta do receptor não aparece na tela. Esgotadas as tentativas, o criador do alerta e os administradores do Tenant recebem uma notificação no Lumo.
O Lumo não publica lista de IPs de origem; a autenticação tem que vir do cabeçalho ou do caminho secreto.