Buscar K
Aparência
Aparência
Esta é a referência completa do que o Lumo envia quando um alerta com canal Webhook ativo dispara. Use esta página como contrato técnico: aqui estão todos os campos, formatos e variações possíveis.
INFO
Tudo aqui é descrito a partir do que o Lumo realmente envia hoje. Quando algo é opcional ou condicional, está marcado. Quando algo está no roadmap (ainda não disponível), está em Avançado → Limitações.
Toda entrega de alerta para webhook é uma requisição HTTP por destinatário por entrega:
| Característica | Valor |
|---|---|
| Método | POST (fixo) |
| URL | A URL configurada no tenant (mesma para todos os alertas do tenant) |
| Content-Type | application/json; charset=utf-8 (fixo, gerenciado pelo sistema) |
| User-Agent | Horus-Alert-Webhook/1.0 (fixo, gerenciado pelo sistema) |
| Corpo | JSON do envelope (descrito abaixo) |
| Timeout | 10 segundos |
| Tamanho máx. do corpo | 4 MB |
WARNING
Se o seu endpoint não responder em 10 segundos, a requisição é abortada e contabilizada como falha (com retry posterior). Endpoints lentos devem responder imediatamente (HTTP 2xx) e processar de forma assíncrona em background.
O Lumo envia dois grupos de headers em cada POST:
1. Headers do sistema (fixos, não configuráveis):
| Header | Valor |
|---|---|
Content-Type | application/json |
User-Agent | Horus-Alert-Webhook/1.0 |
2. Headers personalizados (configuráveis pelo cliente):
Configurados em HEC → Tenants → Editar Tenant → Canais de Alerta Permitidos → Webhook → Headers HTTP Customizados. Os mesmos headers podem ser configurados a nível de cliente (template), e cada tenant herda os do cliente até definir os próprios.
Exemplos comuns:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Custom-Source: lumo-alerts
X-Tenant-Identifier: cliente-acmeCasos de uso típicos:
X-Forwarded-Target, headers de tenant).Limites e regras:
| Regra | Valor |
|---|---|
| Máximo de headers | 20 por tenant |
| Tamanho do nome | até 100 caracteres |
| Tamanho do valor | até 500 caracteres |
| Caracteres permitidos no nome | letras, dígitos e !#$%&'*+-.^_\|~` (token RFC 7230) |
| Headers reservados (não podem ser sobrescritos) | Content-Type, User-Agent, Host, Content-Length, Connection |
INFO
Os headers reservados acima são gerenciados pelo sistema/HTTP stack. Mesmo que sejam configurados na UI por engano, eles são bloqueados na validação antes de salvar — e, em caso de conflito, os valores do sistema sempre prevalecem.
200 e 299 (inclusive).A resposta do seu endpoint é lida e armazenada (até 10 KB; conteúdo além disso é truncado). Isso ajuda a investigar falhas no histórico do alerta.
Todo payload do Lumo segue o mesmo envelope:
{
"alert": { "id": 123, "nome": "Margem Negativa Diária", "type": "condition", "app_id": 456 },
"user": { "id": 789, "nome": "Maria Souza", "email": "maria@empresa.com" },
"tenantId": 1,
"timestamp": "2026-05-27T10:30:00.000Z",
"content": [ /* blocos de conteúdo, descritos abaixo */ ]
}| Campo | Tipo | Descrição |
|---|---|---|
alert.id | número | Identificador único do alerta no Lumo |
alert.nome | string | Nome do alerta como cadastrado pelo usuário |
alert.type | "condition" | "trigger" | Metadado de origem: condicional (dispara por regra) ou agendado (dispara por horário). Não muda o formato do payload. |
alert.app_id | número | null | Aplicação associada ao alerta. Pode ser null para alertas globais (sem app vinculado) |
user.id | número | Identificador do destinatário no Lumo. Sempre presente. |
user.nome | string | Nome do destinatário. Pode estar ausente se o registro tiver sido removido |
user.email | string | E-mail do destinatário (usado como login). Pode estar ausente se o registro tiver sido removido |
tenantId | número | Identificador do tenant que disparou o alerta |
timestamp | string (ISO 8601) | Momento em que a entrega foi enfileirada para envio, em UTC |
content | array | Blocos de conteúdo do alerta, em ordem de exibição (igual à ordem definida no editor) |
TIP
Use tenantId para roteamento se o seu endpoint atende vários tenants do Lumo (ex.: um endpoint compartilhado entre filiais). Use alert.id + timestamp como chave de idempotência (veja Avançado → Idempotência).
content[]) content é um array. Cada item descreve um bloco de conteúdo do alerta. A ordem do array é a mesma da mensagem entregue (espelha o que o usuário vê no editor).
Todos os itens têm o mesmo esqueleto:
{
"id": 11,
"type": "text",
"nome": "Cabeçalho",
"generated": { /* varia por type */ }
}| Campo | Tipo | Descrição |
|---|---|---|
id | número | Identificador único do bloco no alerta |
type | "text" | "ai" | "chart" | "report" | Tipo do bloco. Define o formato de generated. |
nome | string | Nome do bloco, definido pelo usuário no editor (ex.: "Cabeçalho", "Análise IA") |
generated | objeto | Resultado da geração do bloco. Pode estar vazio ({}) se a geração falhou. |
INFO
Comportamento de falha por bloco: chaves vazias não aparecem em generated. Não há null nem undefined explícitos — basta verificar presença da chave. Se a geração de um bloco inteiro falhou, generated pode vir como {}.
type type: "text" Texto fixo configurado pelo usuário, com variáveis renderizadas ( etc. já substituídas pelos valores do alerta).
{
"id": 11,
"type": "text",
"nome": "Cabeçalho",
"generated": {
"text": "3 vendas com margem negativa em Loja Centro"
}
}| Campo | Tipo | Descrição |
|---|---|---|
generated.text | string | Texto final, com variáveis já substituídas |
type: "ai" Análise gerada pelo agente IA. Inclui o texto final e a trilha de passos (passos de pensamento que o agente executou). A trilha permite que o seu receptor referencie buscas e gráficos gerados durante a análise.
{
"id": 12,
"type": "ai",
"nome": "Análise IA",
"generated": {
"text": "Resumo: 3 vendas com prejuízo total de R$ 1.230 em Loja Centro nas últimas 24h. Recomendo revisar política de desconto manual.",
"steps": [
{
"kind": "query",
"title": "Vendas com margem negativa nas últimas 24h",
"chartUrl": "https://storage.horusbi.com.br/download/a1b2c3.png",
"permalink": "https://lumo.exemplo.com/app/12/report?filter=...",
"rowCount": 3,
"summary": "3 linhas retornadas"
},
{
"kind": "search",
"searchText": "filial centro",
"appName": "Vendas",
"columnLabel": "Filial",
"resultCount": 1
}
]
}
}| Campo | Tipo | Descrição |
|---|---|---|
generated.text | string | Texto final da análise (resposta consolidada do agente) |
generated.steps | array | Passos intermediários do agente. Pode ser vazio ([]) |
steps[] Cada passo tem um campo kind que define o formato. Hoje existem duas variantes:
B.1 — kind: "query" (consulta a uma aplicação):
| Campo | Tipo | Descrição |
|---|---|---|
kind | "query" | Tipo do passo |
title | string | Título descritivo da consulta (gerado pela IA) |
chartUrl | string | ausente | URL do gráfico PNG gerado para esta consulta. Pode estar ausente se a IA não gerou gráfico |
permalink | string | Link para abrir esta mesma consulta no Lumo (com filtros aplicados) |
rowCount | número | Quantidade de linhas retornadas pela consulta |
summary | string | Resumo textual do resultado |
B.2 — kind: "search" (validação de dimensão / valor):
| Campo | Tipo | Descrição |
|---|---|---|
kind | "search" | Tipo do passo |
searchText | string | Texto que a IA buscou |
appName | string | Nome da aplicação onde a IA buscou |
columnLabel | string | Coluna em que a busca foi feita |
resultCount | número | Quantos valores foram encontrados |
TIP
Novos kind podem ser adicionados no futuro. Trate kind como enum aberto: se receber um valor desconhecido, ignore o passo e siga em frente — o generated.text continua válido.
type: "report" Relatório exportado. pdf e xlsx são independentemente opcionais — o bloco pode conter só PDF, só XLSX, os dois, ou nenhum (se a geração falhou).
{
"id": 13,
"type": "report",
"nome": "Detalhamento",
"generated": {
"pdf": "https://storage.horusbi.com.br/download/a1b2c3.pdf",
"xlsx": "https://storage.horusbi.com.br/download/a1b2c3.xlsx"
}
}| Campo | Tipo | Descrição |
|---|---|---|
generated.pdf | string | ausente | URL do PDF |
generated.xlsx | string | ausente | URL do XLSX |
WARNING
Sempre verifique presença das chaves antes de usar. Um relatório com apenas Excel terá generated: { "xlsx": "..." } (sem pdf). Um relatório que falhou na geração de ambos terá generated: {}.
type: "chart" O tipo chart tem duas variantes muito diferentes. Você precisa olhar o conteúdo de generated para distinguir.
Snapshot de um widget isolado. Sempre PNG.
{
"id": 14,
"type": "chart",
"nome": "Gráfico de Margem",
"generated": {
"chart": "https://storage.horusbi.com.br/download/a1b2c3.png"
}
}| Campo | Tipo | Descrição |
|---|---|---|
generated.chart | string | URL do PNG |
Snapshot de um dashboard completo. Pode ser PDF ou outro formato.
{
"id": 15,
"type": "chart",
"nome": "Dashboard Operacional",
"generated": {
"kind": "dashboard",
"format": "pdf",
"url": "https://storage.horusbi.com.br/download/a1b2c3.pdf"
}
}| Campo | Tipo | Descrição |
|---|---|---|
generated.kind | "dashboard" | Marca explícita de variante |
generated.format | string | Formato do arquivo (ex.: "pdf") |
generated.url | string | ausente | URL do arquivo. Pode estar ausente em caso de falha |
generated.error | string | ausente | Mensagem de erro, presente quando a geração falhou |
Em falha:
{
"id": 15,
"type": "chart",
"nome": "Dashboard Operacional",
"generated": {
"kind": "dashboard",
"format": "pdf",
"error": "Tempo limite excedido ao gerar o dashboard"
}
}TIP
Como diferenciar as variantes de chart:
if ("kind" in item.generated && item.generated.kind === "dashboard") {
// D.2 — Dashboard
} else if ("chart" in item.generated) {
// D.1 — Widget único
}Todas as URLs de arquivos (PDF, XLSX, PNG) apontam para https://storage.horusbi.com.br/download/....
DANGER
Essas URLs NÃO são permanentes. Não expiram em tempo fixo, mas há limpeza periódica do storage. Um arquivo gerado pode deixar de existir após algumas semanas.
Recomendação: ao receber o payload, baixe e armazene imediatamente os arquivos no seu próprio storage. Não persista a URL do Lumo no seu banco de dados como se fosse permanente.
Boas práticas para download:
content[]: a ordem dos blocos é estável e espelha o editor.alert.id + timestamp) pode chegar 2+ vezes.Esquema formal (JSON Schema Draft 2020-12) para validação no seu receptor:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["alert", "user", "tenantId", "timestamp", "content"],
"properties": {
"alert": {
"type": "object",
"required": ["id", "nome", "type"],
"properties": {
"id": { "type": "integer" },
"nome": { "type": "string" },
"type": { "type": "string", "enum": ["condition", "trigger"] },
"app_id": { "type": ["integer", "null"] }
}
},
"user": {
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "integer" },
"nome": { "type": "string" },
"email": { "type": "string" }
}
},
"tenantId": { "type": "integer" },
"timestamp": { "type": "string", "format": "date-time" },
"content": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "type", "nome", "generated"],
"properties": {
"id": { "type": "integer" },
"type": { "type": "string", "enum": ["text", "ai", "chart", "report"] },
"nome": { "type": "string" },
"generated": {
"type": "object",
"oneOf": [
{
"title": "text",
"properties": { "text": { "type": "string" } }
},
{
"title": "ai",
"properties": {
"text": { "type": "string" },
"steps": {
"type": "array",
"items": {
"type": "object",
"required": ["kind"],
"properties": {
"kind": { "type": "string" }
}
}
}
}
},
{
"title": "report",
"properties": {
"pdf": { "type": "string", "format": "uri" },
"xlsx": { "type": "string", "format": "uri" }
}
},
{
"title": "chart-widget",
"properties": {
"chart": { "type": "string", "format": "uri" }
}
},
{
"title": "chart-dashboard",
"required": ["kind", "format"],
"properties": {
"kind": { "const": "dashboard" },
"format": { "type": "string" },
"url": { "type": "string", "format": "uri" },
"error": { "type": "string" }
}
}
]
}
}
}
}
}
}Para uso direto no seu receptor em Node.js, Deno ou ambiente TypeScript:
// ---------- Envelope ----------
export interface AlertWebhookPayload {
alert: {
id: number;
nome: string;
type: "condition" | "trigger";
app_id: number | null;
};
user: {
id: number;
nome?: string;
email?: string;
};
tenantId: number;
/** ISO 8601 UTC, ex.: "2026-05-27T10:30:00.000Z" */
timestamp: string;
content: ContentBlock[];
}
// ---------- Blocos de conteúdo ----------
export type ContentBlock =
| TextBlock
| AIBlock
| ReportBlock
| ChartWidgetBlock
| ChartDashboardBlock;
export interface BaseBlock {
id: number;
nome: string;
}
export interface TextBlock extends BaseBlock {
type: "text";
generated: { text: string } | {};
}
export interface AIBlock extends BaseBlock {
type: "ai";
generated:
| {
text: string;
steps: AIStep[];
}
| {};
}
export type AIStep = AIQueryStep | AISearchStep;
export interface AIQueryStep {
kind: "query";
title: string;
chartUrl?: string;
permalink: string;
rowCount: number;
summary: string;
}
export interface AISearchStep {
kind: "search";
searchText: string;
appName: string;
columnLabel: string;
resultCount: number;
}
export interface ReportBlock extends BaseBlock {
type: "report";
generated: {
pdf?: string;
xlsx?: string;
};
}
export interface ChartWidgetBlock extends BaseBlock {
type: "chart";
generated: { chart: string };
}
export interface ChartDashboardBlock extends BaseBlock {
type: "chart";
generated: {
kind: "dashboard";
format: string;
url?: string;
error?: string;
};
}
// Helper para distinguir as 2 variantes de chart
export function isDashboardChart(
b: ChartWidgetBlock | ChartDashboardBlock
): b is ChartDashboardBlock {
return "kind" in b.generated && b.generated.kind === "dashboard";
}