Skip to content

Como Receber e Testar

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:

  1. Inspecionar o payload manualmente com webhook.site para entender o que chega.
  2. Testar com curl, simulando o que o Lumo envia.
  3. Automatizar com uma ferramenta low-code (n8n, Make, Zapier) — cobre a maioria dos casos.
  4. Programar um receptor próprio em Node.js ou Python para casos complexos.
  5. Hospedar sem servidor próprio usando Cloudflare Workers ou Vercel Serverless.

webhook.site (inspeção rápida)

A forma mais rápida de ver o payload real chegando, sem escrever uma linha de código.

Passos:

  1. Acesse https://webhook.site. Uma URL única (https://webhook.site/<uuid>) é gerada automaticamente.
  2. Copie essa URL.
  3. No Lumo, em HEC → Tenants → [seu tenant] → Canais de Alerta Permitidos → URL do Webhook, cole a URL e salve.
  4. Em qualquer alerta com canal Webhook ativo, clique em Forçar envio agora (o botão "Pré-visualizar" não dispara POST — apenas renderiza na tela).
  5. Volte para a aba do webhook.site — o request aparece em tempo real, com headers, corpo JSON e timing.

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


curl (simulação local)

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

bash
curl -X POST \
  -H "Content-Type: application/json" \
  -H "User-Agent: Horus-Alert-Webhook/1.0" \
  --data @payload.json \
  https://seu-endpoint.exemplo.com/webhook

Para teste local com seu receptor rodando em localhost:3000, exponha com um tunnel:

bash
# com cloudflared
cloudflared tunnel --url http://localhost:3000

# ou com ngrok
ngrok http 3000

A URL pública gerada pode ser cadastrada como URL do Webhook do tenant para testes ponta-a-ponta.


n8n (low-code)

n8n é a opção mais flexível para integrar o Lumo a sistemas externos sem manter um servidor próprio. Fluxo típico:

Passo a passo

1. Criar o trigger:

  • Adicione o nó Webhook.
  • Método: POST.
  • Path: deixe um valor secreto (ex.: 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.
  • Response Mode: "Last Node" (deixa o n8n responder 200 OK após processar) ou "Immediately" (responde 200 antes mesmo de processar — recomendado para evitar timeout de 10s).

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:

DestinoNó n8nO que mandar
WhatsApp via MetaHTTP RequestPOST https://graph.facebook.com/v20.0/<phone-number-id>/messages
SlackHTTP Request ou SlackPOST no Incoming Webhook do canal
Microsoft TeamsHTTP RequestPOST no Webhook Connector do canal
Planilha de logGoogle SheetsAppend da linha com alert.nome, timestamp, user.email
Banco internoPostgres / MySQLINSERT 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).


Make (ex-Integromat)

Funcionalmente equivalente ao n8n, com uma UI mais visual.

Passo a passo:

  1. Crie um novo Scenario → módulo inicial Webhooks → Custom webhook.
  2. Clique em Add, dê um nome (ex.: "Lumo Alerts"). O Make gera uma URL pública.
  3. Cole essa URL como URL do Webhook do tenant no Lumo.
  4. Force um envio no Lumo (botão Forçar envio agora — "Pré-visualizar" não envia POST) e o Make detecta a estrutura do payload automaticamente.
  5. Encadeie módulos: Router (para condicionais), HTTP → Make a request (para chamar APIs externas), WhatsApp Business, Slack, etc.

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.


Zapier

Mais simples, mas com menos flexibilidade.

Passo a passo:

  1. Crie um novo Zap → trigger Webhooks by Zapier → Catch Hook.
  2. Copie a URL gerada e cole no Lumo como URL do Webhook do tenant.
  3. Force um envio para o Zapier capturar a estrutura do payload.
  4. Adicione ações (ex.: WhatsApp by Zapier, Slack, Google Sheets).

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.


Node.js + Express

Servidor próprio mínimo para casos em que você precisa de lógica que não cabe em ferramentas no-code.

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


Python + FastAPI

Equivalente em Python, idiomatic para quem prefere async.

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


Cloudflare Workers (serverless)

Bom quando você quer um endpoint baratíssimo, com TLS e escalonamento automático, sem manter VM. Disponível no plano gratuito.

javascript
// 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.


Vercel Serverless Functions

Padrão equivalente, usando Next.js Route Handlers ou Vercel Functions puras:

typescript
// 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.


Próximos Passos