Buscar K
Aparência
Aparência
Esta página mostra como receber e processar o webhook do Lumo em diferentes ferramentas, do mais simples (inspeção manual no navegador) ao mais robusto (servidor próprio em produção).
Use a ordem desta página como um roteiro de adoção:
A forma mais rápida de ver o payload real chegando, sem escrever uma linha de código.
Passos:
https://webhook.site/<uuid>) é gerada automaticamente.TIP
O webhook.site é público. Se o seu payload tiver dados sensíveis (e em produção sempre tem), use só para testes pontuais com dados sintéticos. Para inspeção em ambiente real, monte um receptor próprio (veja seções abaixo).
Útil para simular o que o Lumo envia, sem precisar disparar um alerta real. Copie um payload da referência ou do histórico de uma entrega já enviada, salve como payload.json e:
curl -X POST \
-H "Content-Type: application/json" \
-H "User-Agent: Horus-Alert-Webhook/1.0" \
--data @payload.json \
https://seu-endpoint.exemplo.com/webhookPara teste local com seu receptor rodando em localhost:3000, exponha com um tunnel:
# com cloudflared
cloudflared tunnel --url http://localhost:3000
# ou com ngrok
ngrok http 3000A URL pública gerada pode ser cadastrada como URL do Webhook do tenant para testes ponta-a-ponta.
n8n é a opção mais flexível para integrar o Lumo a sistemas externos sem manter um servidor próprio. Fluxo típico:
1. Criar o trigger:
POST.lumo-alerts-7f3a9b2e). A URL final será https://seu-n8n.exemplo.com/webhook/lumo-alerts-7f3a9b2e — esse path secreto serve como validação de origem.2. Cadastrar a URL no Lumo:
Copie a URL de produção do nó Webhook e cole em HEC → Tenants → Canais de Alerta Permitidos → URL do Webhook.
3. Roteamento:
Use um nó Switch com expressões que olham $json.alert.nome ou $json.alert.id para encaminhar o payload a diferentes destinos.
4. Destinos comuns:
| Destino | Nó n8n | O que mandar |
|---|---|---|
| WhatsApp via Meta | HTTP Request | POST https://graph.facebook.com/v20.0/<phone-number-id>/messages |
| Slack | HTTP Request ou Slack | POST no Incoming Webhook do canal |
| Microsoft Teams | HTTP Request | POST no Webhook Connector do canal |
| Planilha de log | Google Sheets | Append da linha com alert.nome, timestamp, user.email |
| Banco interno | Postgres / MySQL | INSERT na tabela de auditoria |
TIP
No n8n, configure Response Mode: Immediately no nó Webhook e processe em background. Isso evita que o Lumo dê timeout (10s) caso a sua automação seja lenta (ex.: Meta API pode ter latência).
Funcionalmente equivalente ao n8n, com uma UI mais visual.
Passo a passo:
INFO
O Make tem um limite de 40 segundos para processar um cenário síncrono, mas o Lumo tem timeout de 10 segundos. Use Webhook Response logo após o webhook inicial para responder 200 antes de processar.
Mais simples, mas com menos flexibilidade.
Passo a passo:
WARNING
O Zapier responde 200 de imediato apenas no plano gratuito de Catch Hook. Em workflows com filtros e ações pesadas, considere usar Webhooks by Zapier → Catch Raw Hook e responder você mesmo.
Servidor próprio mínimo para casos em que você precisa de lógica que não cabe em ferramentas no-code.
import express from "express";
import type { Request, Response } from "express";
const app = express();
// limite explícito para casar com os 4 MB do Lumo
app.use(express.json({ limit: "4mb" }));
// (Opcional, mas recomendado) caminho secreto como validação de origem.
// Veja: ../advanced.md#caminho-secreto-na-url
app.post("/webhook/lumo/:secret", async (req: Request, res: Response) => {
if (req.params.secret !== process.env.LUMO_WEBHOOK_SECRET) {
return res.status(404).end(); // não revele que o caminho existe
}
// 1. Responde IMEDIATAMENTE para evitar timeout de 10s
res.status(200).json({ received: true });
// 2. Processa em background (não bloqueia a resposta)
try {
await processarAlerta(req.body);
} catch (err) {
console.error("[lumo] falha ao processar alerta", err);
}
});
async function processarAlerta(payload: any) {
const { alert, user, content, timestamp } = payload;
console.log(`[lumo] ${alert.nome} → ${user.email ?? user.id} @ ${timestamp}`);
for (const bloco of content) {
switch (bloco.type) {
case "text":
case "ai":
console.log(`[texto] ${bloco.nome}: ${bloco.generated.text}`);
break;
case "report":
if (bloco.generated.pdf) {
await baixarEArmazenar(bloco.generated.pdf, "pdf");
}
if (bloco.generated.xlsx) {
await baixarEArmazenar(bloco.generated.xlsx, "xlsx");
}
break;
case "chart":
if ("kind" in bloco.generated && bloco.generated.kind === "dashboard") {
if (bloco.generated.url) {
await baixarEArmazenar(bloco.generated.url, bloco.generated.format);
}
} else if ("chart" in bloco.generated) {
await baixarEArmazenar(bloco.generated.chart, "png");
}
break;
}
}
}
async function baixarEArmazenar(url: string, extensao: string) {
// Importante: URLs do Lumo NÃO são permanentes. Baixe e guarde no seu storage.
const resp = await fetch(url);
if (!resp.ok) throw new Error(`download falhou: ${resp.status}`);
const buffer = await resp.arrayBuffer();
// Aqui você grava em S3, disco, banco, etc.
console.log(`baixados ${buffer.byteLength} bytes (.${extensao})`);
}
app.listen(3000, () => console.log("ouvindo em :3000"));TIP
O padrão "responder 200 imediatamente, processar em background" é o mais seguro contra o timeout de 10 segundos do Lumo. Use uma fila (BullMQ, RabbitMQ, SQS) se o processamento for pesado.
Equivalente em Python, idiomatic para quem prefere async.
import os
import asyncio
import httpx
from fastapi import FastAPI, Request, BackgroundTasks, HTTPException
app = FastAPI()
LUMO_SECRET = os.getenv("LUMO_WEBHOOK_SECRET", "")
@app.post("/webhook/lumo/{secret}")
async def receber_alerta(
secret: str,
request: Request,
background_tasks: BackgroundTasks,
):
if secret != LUMO_SECRET:
raise HTTPException(status_code=404)
payload = await request.json()
# Agenda processamento em background e responde 200 imediatamente
background_tasks.add_task(processar_alerta, payload)
return {"received": True}
async def processar_alerta(payload: dict) -> None:
alert = payload["alert"]
user = payload.get("user", {})
content = payload.get("content", [])
print(f"[lumo] {alert['nome']} -> {user.get('email', user.get('id'))}")
async with httpx.AsyncClient(timeout=30) as client:
for bloco in content:
tipo = bloco["type"]
gen = bloco.get("generated", {})
if tipo in ("text", "ai") and "text" in gen:
print(f"[texto] {bloco['nome']}: {gen['text']}")
elif tipo == "report":
for ext in ("pdf", "xlsx"):
if ext in gen:
await baixar(client, gen[ext], ext)
elif tipo == "chart":
if gen.get("kind") == "dashboard" and "url" in gen:
await baixar(client, gen["url"], gen.get("format", "bin"))
elif "chart" in gen:
await baixar(client, gen["chart"], "png")
async def baixar(client: httpx.AsyncClient, url: str, ext: str) -> None:
# URLs do Lumo nao sao permanentes. Salve no seu storage agora.
resp = await client.get(url)
resp.raise_for_status()
print(f"baixados {len(resp.content)} bytes (.{ext})")INFO
Em produção, troque BackgroundTasks por uma fila persistente (Celery, RQ, Arq) — BackgroundTasks perde tarefas se o processo cair.
Bom quando você quer um endpoint baratíssimo, com TLS e escalonamento automático, sem manter VM. Disponível no plano gratuito.
// worker.js — deploy via `wrangler deploy`
export default {
async fetch(request, env, ctx) {
if (request.method !== "POST") {
return new Response("Method Not Allowed", { status: 405 });
}
const url = new URL(request.url);
if (url.pathname !== `/webhook/lumo/${env.LUMO_SECRET}`) {
return new Response("Not Found", { status: 404 });
}
const payload = await request.json();
// ctx.waitUntil permite processar em background depois de responder
ctx.waitUntil(processarAlerta(payload, env));
return new Response(JSON.stringify({ received: true }), {
status: 200,
headers: { "content-type": "application/json" },
});
},
};
async function processarAlerta(payload, env) {
const { alert, user } = payload;
console.log(`[lumo] ${alert.nome} → ${user.email ?? user.id}`);
// Encaminhar para outros endpoints (Meta API, Slack, etc.) com fetch().
}LUMO_SECRET é configurada como variável de ambiente do Worker. A URL final fica https://<seu-worker>.workers.dev/webhook/lumo/<secret> e é cadastrada como URL do Webhook do tenant.
Padrão equivalente, usando Next.js Route Handlers ou Vercel Functions puras:
// app/api/webhook/lumo/[secret]/route.ts
import { NextResponse } from "next/server";
export async function POST(
request: Request,
{ params }: { params: { secret: string } }
) {
if (params.secret !== process.env.LUMO_WEBHOOK_SECRET) {
return NextResponse.json({ error: "not found" }, { status: 404 });
}
const payload = await request.json();
// Vercel Functions têm timeout de execução; despache pra fila externa
// (Upstash QStash, Inngest, ou seu próprio worker) e responda já.
await encaminharParaFila(payload);
return NextResponse.json({ received: true });
}
async function encaminharParaFila(payload: any) {
// Exemplo: POST para Upstash QStash, que processa em background.
}WARNING
Funções serverless têm timeout de execução próprio (60s na Vercel, 10s na Cloudflare, 15min em Lambda). Se você processa de forma síncrona, garanta que o seu processamento + a resposta cabem dentro do timeout mais apertado: os 10 segundos do Lumo. O padrão recomendado continua sendo responder 200 imediato, processar em fila externa.