Skip to content

Referência do Payload

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.


Requisição HTTP

Toda entrega de alerta para webhook é uma requisição HTTP por destinatário por entrega:

CaracterísticaValor
MétodoPOST (fixo)
URLA URL configurada no tenant (mesma para todos os alertas do tenant)
Content-Typeapplication/json; charset=utf-8 (fixo, gerenciado pelo sistema)
User-AgentHorus-Alert-Webhook/1.0 (fixo, gerenciado pelo sistema)
CorpoJSON do envelope (descrito abaixo)
Timeout10 segundos
Tamanho máx. do corpo4 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.

Headers HTTP que sua URL recebe

O Lumo envia dois grupos de headers em cada POST:

1. Headers do sistema (fixos, não configuráveis):

HeaderValor
Content-Typeapplication/json
User-AgentHorus-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:

http
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Custom-Source: lumo-alerts
X-Tenant-Identifier: cliente-acme

Casos de uso típicos:

  • Autenticação do request no seu endpoint (token bearer, chave de API).
  • Roteamento em proxies/middlewares (X-Forwarded-Target, headers de tenant).
  • Identificação de origem em serviços compartilhados que recebem webhooks de várias fontes.

Limites e regras:

RegraValor
Máximo de headers20 por tenant
Tamanho do nomeaté 100 caracteres
Tamanho do valoraté 500 caracteres
Caracteres permitidos no nomeletras, 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.

Sucesso vs Falha

  • Sucesso: o seu endpoint retornou um status HTTP entre 200 e 299 (inclusive).
  • Falha: qualquer outro status, timeout, erro de DNS, recusa de conexão, certificado inválido, ou corpo da requisição maior que 4 MB.

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.


Envelope (raiz do JSON)

Todo payload do Lumo segue o mesmo envelope:

json
{
  "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 */ ]
}

Campos da raiz

CampoTipoDescrição
alert.idnúmeroIdentificador único do alerta no Lumo
alert.nomestringNome 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_idnúmero | nullAplicação associada ao alerta. Pode ser null para alertas globais (sem app vinculado)
user.idnúmeroIdentificador do destinatário no Lumo. Sempre presente.
user.nomestringNome do destinatário. Pode estar ausente se o registro tiver sido removido
user.emailstringE-mail do destinatário (usado como login). Pode estar ausente se o registro tiver sido removido
tenantIdnúmeroIdentificador do tenant que disparou o alerta
timestampstring (ISO 8601)Momento em que a entrega foi enfileirada para envio, em UTC
contentarrayBlocos 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).


Bloco de Conteúdo (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:

json
{
  "id":        11,
  "type":      "text",
  "nome":      "Cabeçalho",
  "generated": { /* varia por type */ }
}
CampoTipoDescrição
idnúmeroIdentificador único do bloco no alerta
type"text" | "ai" | "chart" | "report"Tipo do bloco. Define o formato de generated.
nomestringNome do bloco, definido pelo usuário no editor (ex.: "Cabeçalho", "Análise IA")
generatedobjetoResultado 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 {}.


Detalhamento por type

A. type: "text"

Texto fixo configurado pelo usuário, com variáveis renderizadas ( etc. já substituídas pelos valores do alerta).

json
{
  "id": 11,
  "type": "text",
  "nome": "Cabeçalho",
  "generated": {
    "text": "3 vendas com margem negativa em Loja Centro"
  }
}
CampoTipoDescrição
generated.textstringTexto final, com variáveis já substituídas

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

json
{
  "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
      }
    ]
  }
}
CampoTipoDescrição
generated.textstringTexto final da análise (resposta consolidada do agente)
generated.stepsarrayPassos intermediários do agente. Pode ser vazio ([])

Variantes de steps[]

Cada passo tem um campo kind que define o formato. Hoje existem duas variantes:

B.1 — kind: "query" (consulta a uma aplicação):

CampoTipoDescrição
kind"query"Tipo do passo
titlestringTítulo descritivo da consulta (gerado pela IA)
chartUrlstring | ausenteURL do gráfico PNG gerado para esta consulta. Pode estar ausente se a IA não gerou gráfico
permalinkstringLink para abrir esta mesma consulta no Lumo (com filtros aplicados)
rowCountnúmeroQuantidade de linhas retornadas pela consulta
summarystringResumo textual do resultado

B.2 — kind: "search" (validação de dimensão / valor):

CampoTipoDescrição
kind"search"Tipo do passo
searchTextstringTexto que a IA buscou
appNamestringNome da aplicação onde a IA buscou
columnLabelstringColuna em que a busca foi feita
resultCountnúmeroQuantos 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.


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

json
{
  "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"
  }
}
CampoTipoDescrição
generated.pdfstring | ausenteURL do PDF
generated.xlsxstring | ausenteURL 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: {}.


D. type: "chart"

O tipo chart tem duas variantes muito diferentes. Você precisa olhar o conteúdo de generated para distinguir.

D.1 — Widget único (PNG)

Snapshot de um widget isolado. Sempre PNG.

json
{
  "id": 14,
  "type": "chart",
  "nome": "Gráfico de Margem",
  "generated": {
    "chart": "https://storage.horusbi.com.br/download/a1b2c3.png"
  }
}
CampoTipoDescrição
generated.chartstringURL do PNG

D.2 — Dashboard inteiro

Snapshot de um dashboard completo. Pode ser PDF ou outro formato.

json
{
  "id": 15,
  "type": "chart",
  "nome": "Dashboard Operacional",
  "generated": {
    "kind":   "dashboard",
    "format": "pdf",
    "url":    "https://storage.horusbi.com.br/download/a1b2c3.pdf"
  }
}
CampoTipoDescrição
generated.kind"dashboard"Marca explícita de variante
generated.formatstringFormato do arquivo (ex.: "pdf")
generated.urlstring | ausenteURL do arquivo. Pode estar ausente em caso de falha
generated.errorstring | ausenteMensagem de erro, presente quando a geração falhou

Em falha:

json
{
  "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:

ts
if ("kind" in item.generated && item.generated.kind === "dashboard") {
  // D.2 — Dashboard
} else if ("chart" in item.generated) {
  // D.1 — Widget único
}

URLs de Download

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:

  • Use HTTP GET; nenhum header de autenticação é necessário.
  • Tamanhos típicos: PNG até alguns MB, PDF/XLSX podem chegar a dezenas de MB.
  • Em caso de 404, considere a entrega "URL expirada" e marque como degradada no seu lado (mas a entrega original do Lumo ainda foi um sucesso).

Garantias e Não-Garantias

O que o Lumo garante

  • Entrega pelo menos uma vez (at-least-once) por destinatário: a mesma entrega pode chegar várias vezes em caso de retry com resposta lenta. Implemente idempotência (detalhes).
  • Ordem dentro de content[]: a ordem dos blocos é estável e espelha o editor.
  • Encoding UTF-8 no corpo JSON.

O que o Lumo NÃO garante

  • Ordem entre entregas: se dois alertas dispararem quase ao mesmo tempo, eles podem chegar ao seu endpoint fora da ordem cronológica de disparo.
  • Unicidade por destinatário: em retry agressivo, o mesmo payload (mesmo alert.id + timestamp) pode chegar 2+ vezes.
  • Permanência das URLs de arquivos: como dito acima, baixe assim que receber.

JSON Schema completo

Esquema formal (JSON Schema Draft 2020-12) para validação no seu receptor:

json
{
  "$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" }
                }
              }
            ]
          }
        }
      }
    }
  }
}

Tipos TypeScript

Para uso direto no seu receptor em Node.js, Deno ou ambiente TypeScript:

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";
}

Próximos Passos