Catalisa.Building Blocks
Catálogo/Comunicação/Slack

Slack

Alpha

Mensagens do Slack dos seus clientes entrando e saindo do seu produto

8
Endpoints
2
Entidades
1
Provedores
Tenant
Escopo
3029
Porta
2026-06
Desde

Seu produto passa a ler e responder mensagens no Slack de cada cliente, com o workspace dele conectado por OAuth, sem que você construa um app Slack, um verificador de assinatura e uma fila de entrega do zero.

Para quem é
  • Produtos de assistente ou copiloto que precisam conversar com o usuário onde ele já está
  • Plataformas B2B cujo cliente quer receber alerta operacional no canal do time dele
  • Times que já usam o WPP ou o WPP Business da Catalisa e querem o Slack como mais um canal
Substitui
  • Um app Slack próprio com OAuth, verificação de assinatura e gestão de token por workspace
  • Assinatura de uma ferramenta de automação (Zapier, Make) só para levar mensagem de Slack ao seu backend
  • Camada caseira de filtro de ruído para não inundar seu sistema com toda mensagem do workspace
O que não é
  • Uma plataforma de notificação multicanal (isso é o Webhooks Engine somado aos BBs de canal)
  • Um bot pronto ou um framework de conversa — o building block só transporta a mensagem
  • Um arquivo de mensagens do Slack — nada de conteúdo de conversa é persistido aqui
  • Um substituto do Slack Connect ou do Slack Enterprise Grid
O que dá para fazer

10 endpoints em 5 recursos.

Explorar a API →
01

Resumo executivo

O Slack conecta o workspace de cada cliente ao seu produto nos dois sentidos. O usuário autoriza uma vez pelo botão do Slack, e a partir daí as mensagens que interessam — DMs, menções e os canais que ele escolheu observar — chegam ao seu backend por webhook, e o seu produto pode responder no mesmo lugar.

Na prática: um assistente que já atende pelo WhatsApp passa a atender pelo Slack sem nova arquitetura. O usuário manda "qual o status da proposta 4471?" numa DM, o evento chega ao seu endpoint em segundos, e a resposta volta pelo POST /messages/send como se tivesse sido digitada pelo próprio usuário.

Está em alpha. São 8 endpoints, e a lista do que falta é longa e honesta: não há como desconectar um workspace pela API, não há listar nem apagar dispatcher, o token não é renovado, e o app Slack ainda depende de aprovação da Slack para ser instalado fora do workspace de desenvolvimento. Ler a §15 antes de prometer qualquer coisa a cliente não é recomendação, é requisito.

AtributoValor
Identificadorslack
CategoriaComunicação
EscopoTenant (exige organizationId no token)
Porta (standalone)3029
Path alias@slack
Prefixo HTTP/slack/api/v1/slack — sim, slack aparece duas vezes
StatusAlpha desde 2026-06
Depende dePostgreSQL (schema slack), IAM, Webhooks Engine, Slack Platform

Atenção ao prefixo. O app Hono usa basePath('/slack') e os routers são montados em /api/v1/slack/.... A rota completa fica /slack/api/v1/slack/messages/send, com o nome do módulo repetido. É o que o código faz hoje. Toda rota da §9 está na forma completa e real.


02

O problema

negócio

O cenário. Seu produto precisa falar com o usuário dentro do Slack da empresa dele. Não o seu Slack — o dele. E não um workspace, mas o de cada cliente que você atende, cada um com seu próprio consentimento, seus próprios canais e sua própria política de segurança.

O que trava hoje.

  • Um app Slack de verdade é um projeto, não um endpoint. Para instalar em mais de um workspace, o app precisa de distribuição pública ativada, o que a Slack condiciona a uma lista de requisitos: "Your app must support SSL for all of the following URLs", fluxo de onboarding para múltiplos workspaces, e pedir só as permissões necessárias, "admins screen these during approval" (Slack — Distributing Slack apps, consulta em 2026-08-16). Depois disso vem OAuth por workspace e um token por instalação para guardar e cifrar.
  • A janela de resposta é de três segundos. A Slack é explícita: você deve "respond to the event request with an HTTP 2xx within three seconds", e falhando isso ela reenvia três vezes — "nearly immediately", depois de 1 minuto e depois de 5 minutos (Slack — Events API, consulta em 2026-08-16). Qualquer processamento síncrono dentro do handler vira mensagem duplicada em produção.
  • Verificação de assinatura é obrigatória e fácil de errar. A Slack assina cada requisição, e a verificação exige o corpo bruto — reserializar o JSON antes de calcular o HMAC quebra a comparação de forma silenciosa. Some a isso a janela de tolerância de tempo contra reenvio de requisição capturada.
  • O volume de um workspace inunda qualquer backend. Ligar o Events API sem filtro significa receber toda mensagem de todo canal, incluindo entrada e saída de gente, edição, remoção e mensagem de outros bots. Filtrar isso é código, e é código que precisa rodar antes da fila, não depois.
  • Ferramenta de automação não resolve multi-tenant. Zapier e Make são ótimos para um fluxo. Para cem clientes com cem workspaces, cada um com seus canais, o modelo de cobrança por tarefa e a ausência de contexto de tenant transformam a solução no problema.

O custo de não resolver. A soma é: um app distribuído aprovado, OAuth por workspace, cifra e ciclo de vida de token por instalação, verificação de assinatura com corpo bruto, ack em menos de três segundos com processamento assíncrono, e filtro de ruído. Nada disso é a sua regra de negócio, e tudo isso precisa existir antes do primeiro cliente ver valor.

Posto em forma de lista de tarefas, é o que você entrega antes da primeira funcionalidade visível:

Peça obrigatóriaPor que ela existeÉ regra de negócio sua?
App Slack distribuído e aprovadoSem distribuição pública, o app só instala no seu próprio workspaceNão
OAuth por workspaceCada cliente autoriza o próprio workspace, com o próprio consentimentoNão
Cifra e ciclo de vida de token por instalaçãoUm token por instalação, guardado em repouso e mantido válidoNão
Verificação de assinatura sobre o corpo brutoRota pública sem token da plataforma; a assinatura é a única defesaNão
Ack em menos de 3 segundos, com processamento assíncronoExigência da Slack; falhar nela produz mensagem duplicadaNão
Filtro de ruído antes da filaSem ele, o workspace inteiro entra no seu backendNão

03

Proposta de valor

negócio
AntesDepois
Construir e manter um app Slack distribuído por conta própriaUm app configurado uma vez, com o OAuth por workspace já implementado
Token de instalação guardado como derAES-256-GCM no banco, envelope cifrado, nunca devolvido pela API
Handler que processa dentro dos 3 segundos e reenvia duplicadoAck imediato e processamento disparado fora do caminho da resposta
Todo evento do workspace chegando ao seu backendDM, menção e canais escolhidos — o resto é descartado antes da entrega
Uma integração de entrega por canal (WhatsApp, Slack, ...)O mesmo Webhooks Engine, com assinatura, retry e log, para todos

O building block é bidirecional, e vale ver os dois sentidos separados antes de entrar em cada ganho:

flowchart LR
  subgraph Entrada["Entrada — o usuário fala"]
    U1["Usuário escreve<br/>no Slack"] --> EV["POST /events<br/>assinatura + ack 3s"]
    EV --> FIL["shouldForward<br/>filtro de ruído"]
    FIL --> WE["Webhooks Engine"]
    WE --> BE["Seu backend"]
  end
  subgraph Saida["Saída — o produto responde"]
    BE2["Seu backend"] --> SEND["POST /messages/send"]
    SEND --> SL["chat.postMessage<br/>as_user true"]
    SL --> U2["Mensagem no canal,<br/>com identidade humana"]
  end

OAuth por workspace, pronto

POST /connections/connect devolve a URL de autorização com o state cifrado; o callback troca o código por token, identifica o time e grava a conexão. O integrador não escreve OAuth.

Ack primeiro, trabalho depois

O handler de eventos verifica a assinatura, responde 200 e dispara a ingestão sem esperar. É a arquitetura que a janela de três segundos da Slack exige, e ela já está feita.

Filtro de ruído no lugar certo

A função shouldForward descarta subtipos de ruído (bot_message, message_changed, message_deleted, channel_join, channel_leave), deixa passar toda DM e todo grupo de DM, deixa passar qualquer mensagem que mencione o usuário conectado, e para canais comuns só entrega os que estão em watchedChannels. O que sai daqui é sinal.

Entrega pela infraestrutura que já existe

O dispatcher cria uma assinatura no Webhooks Engine — a mesma que o WPP e o WPP Business usam — com timeout de 10 segundos e três tentativas em backoff exponencial. Assinatura, retry e log de entrega não são reimplementados aqui.


04

Casos de uso reais

negócio

Caso 1 — Um assistente ganha o Slack como canal sem nova arquitetura Cenário ilustrativo

Contexto

Produto de assistente para times de operações que já atendia por WhatsApp usando o building block WPP. Os clientes corporativos pediram Slack.

A dor

A primeira estimativa do time para "colocar Slack" foi de seis a oito semanas: app distribuído, OAuth por workspace, verificação de assinatura, ack em três segundos, filtro e uma fila de entrega própria. Nenhuma dessas semanas produziria uma linha de valor para o usuário — o assistente em si já existia.

A solução com o BB

O cliente conecta o workspace por POST /slack/api/v1/slack/connections/connect. Um POST /slack/api/v1/slack/dispatchers aponta para o mesmo endpoint que já recebia os eventos do WPP, com watchedChannels definindo os canais observados. O evento chega como slack.v1.message.received com o mesmo envelope de dispatcher que o time já sabia consumir. A resposta sai por POST /slack/api/v1/slack/messages/send.

flowchart LR
  WA["WhatsApp<br/>via BB wpp"] --> WE["Webhooks Engine"]
  SK["Slack<br/>via BB slack"] --> WE
  WE --> CONS["Consumidor único<br/>já existente<br/>/hooks/mensagens"]
  CONS --> ASS["Assistente<br/>regra de produto"]
  ASS -->|"resposta"| OUT1["POST /messages/send<br/>Slack"]
  ASS -->|"resposta"| OUT2["Envio WPP"]
O resultado

O canal novo reaproveita o consumidor existente. O trabalho que sobra é de produto — o que o assistente responde no Slack —, não de plataforma.

Caso 2 — Um alerta operacional chega no canal do cliente, não numa caixa de e-mail Cenário ilustrativo

Contexto

Plataforma de logística que avisa quando uma entrega ultrapassa a janela combinada.

A dor

Os avisos iam por e-mail para uma lista. Ninguém lia, porque a decisão sobre a entrega atrasada é tomada no canal #operacao-sp do Slack, onde o time já está. O e-mail chegava, era encaminhado para o Slack por uma pessoa, e o tempo de resposta era o tempo dessa pessoa.

A solução com o BB

O cliente conecta o workspace e a plataforma passa a postar em #operacao-sp por POST /messages/send. Como a mensagem é enviada com o token do usuário que instalou o app (as_user: true), ela aparece com identidade humana no canal, não como bot anônimo — o que muda a taxa de leitura.

sequenceDiagram
  autonumber
  participant P as "Plataforma de logística"
  participant S as "BB Slack"
  participant SL as "Slack — canal #operacao-sp"
  participant T as "Time de operação"
  participant WE as "Webhooks Engine"

  P->>S: "POST /messages/send — entrega 4471 fora da janela"
  S->>SL: "chat.postMessage as_user true"
  SL-->>T: "Aviso aparece com identidade humana"
  T->>SL: "Responde no canal — abrir tratativa"
  SL->>S: "POST /events — assinatura verificada"
  S->>S: "shouldForward — canal está em watchedChannels"
  S->>WE: "slack.v1.message.received"
  WE->>P: "Entrega assinada — vira registro de tratativa"
O resultado

O aviso nasce onde a decisão acontece. E, como o mesmo dispatcher observa o canal, a resposta que o time escreve volta ao produto e vira registro de tratativa.

Caso 3 — O filtro de ruído é o que torna a ingestão viável Cenário ilustrativo

Contexto

Cliente com um workspace de 400 pessoas e cerca de 90 canais ativos.

A dor

Um piloto anterior, feito com uma ferramenta de automação genérica, ligou o Events API sem filtro. O backend recebeu tudo: entrada e saída de gente em canal, edição de mensagem, mensagem de outros bots, thread de canal irrelevante. O volume derrubou a fila no primeiro dia útil, e o custo por tarefa da ferramenta estourou o orçamento do piloto na primeira semana.

A solução com o BB

O shouldForward roda antes da entrega: descarta os cinco subtipos de ruído, entrega toda DM e MPIM, entrega qualquer texto contendo <@USER_ID> do usuário conectado, e para canais comuns exige que o canal esteja em watchedChannels. Os canais observados são ajustados por PATCH /dispatchers/:id sem recriar nada.

flowchart LR
  WS["Workspace<br/>400 pessoas · 90 canais"] --> EV["Todo evento do<br/>Events API"]
  EV --> F{"shouldForward<br/>roda no BB,<br/>antes da entrega"}
  F -->|"bot, editada, apagada,<br/>entrada e saída de canal"| D["Descartado<br/>não trafega, não é cobrado"]
  F -->|"canal comum fora<br/>de watchedChannels"| D
  F -->|"DM e grupo de DM"| E["Entregue ao backend"]
  F -->|"menção ao usuário conectado"| E
  F -->|"canal em watchedChannels"| E

Atenção. A ferramenta genérica do piloto filtrava depois de receber — e cobrava por tudo que recebeu. Aqui o filtro roda dentro do processo do building block, antes da fila e antes de qualquer entrega. Em workspace grande, essa diferença de posição é a diferença entre o projeto caber e não caber no orçamento.

O resultado

Do workspace inteiro, chega ao backend a fração que tem endereço: conversa direta, menção explícita e os canais que alguém escolheu observar.

Caso 4 — Distribuir um app Slack tem um portão, e ele é público Referência de mercado

Contexto

Quem planeja "integrar Slack" costuma orçar a API e esquecer o processo de distribuição.

A dor do mercado

A própria Slack documenta que instalar em outros workspaces exige OAuth 2.0, SSL em todas as URLs e uma checklist de distribuição pública, e que "apps intended for commercial distribution should be submitted and approved for listing in the Slack Marketplace" (Slack — Distributing Slack apps, consulta em 2026-08-16). Distribuição não listada é possível para clientes iniciais, mas escopos de histórico de conversa passam por escrutínio, e o administrador de cada workspace corporativo ainda pode exigir aprovação individual.

Como a Catalisa endereça

O building block implementa o lado técnico inteiro — OAuth v2 com state cifrado, verificação de assinatura, ack de três segundos, escopos de usuário declarados e documentados. O runbook `docs/slack/SETUP.md` descreve o processo de criação do app, o manifesto pronto e a política de aprovação por tipo de workspace.

flowchart LR
  A["Lado técnico<br/>o BB resolve"] --> A1["OAuth v2 com state cifrado"]
  A --> A2["Verificação de assinatura"]
  A --> A3["Ack de três segundos"]
  A --> A4["Escopos declarados<br/>e documentados"]
  B["Portão externo<br/>ninguém resolve pelo cliente"] --> B1["Revisão do app pela Slack"]
  B --> B2["Aprovação do administrador<br/>de cada workspace"]
  B --> B3["Enquanto não sai, o alcance é<br/>o workspace de instalação — §15"]
O resultado

O que resta ao cliente é a parte que ninguém pode fazer por ele: o processo de aprovação junto à Slack e ao administrador do workspace. Ressalva honesta: enquanto a revisão do app não sai, o alcance é o workspace de instalação. Está em §15.


05

Mercado e diferenciais

negócio

Panorama. Colocar Slack dentro de um produto tem três caminhos. Construir o app direto na Slack Platform dá controle total e cobra em engenharia: distribuição, OAuth por workspace, ciclo de vida de token, assinatura, ack de três segundos. Usar uma ferramenta de automação — Zapier, Make, n8n — resolve um fluxo em uma tarde e cobra por execução, com o fluxo vivendo fora do seu código. Contratar uma plataforma de notificação — Knock, Courier — resolve muito bem o caminho de saída e quase não toca o de entrada.

O Slack da Catalisa fica num quarto lugar, estreito de propósito: é o transporte bidirecional por workspace, dentro de uma plataforma que o cliente já usa para identidade e entrega de eventos. Não compete em amplitude com o Zapier nem em orquestração com o Knock.

flowchart TD
  P["Colocar Slack<br/>dentro do produto"] --> C1["Slack Platform direta"]
  P --> C2["Automação<br/>Zapier · Make · n8n"]
  P --> C3["Notificação<br/>Knock · Courier"]
  P --> C4["Catalisa Slack"]
  C1 --> R1["Controle total.<br/>Cobra em engenharia"]
  C2 --> R2["Um fluxo numa tarde.<br/>Cobra por execução,<br/>fluxo fora do seu código"]
  C3 --> R3["Saída muito bem resolvida.<br/>Quase não toca a entrada"]
  C4 --> R4["Transporte bidirecional<br/>por workspace, dentro da<br/>plataforma já contratada"]
CritérioCatalisa SlackSlack API diretaZapiern8nKnock
Ingestão de mensagem do usuárioSim, com filtro de ruídoVocê constróiSim, por triggerSimNão é o foco
Envio de mensagemSim, como o usuário conectadoSimSimSimSim, como bot
Multi-tenant por workspaceSim, ligado à organização do IAMVocê constróiPor conta conectadaVocê constróiPor tenant do produto
Modelo de preçoEm definiçãoSem licença de APIPor tarefa (Free 100/mês; Pro a partir de US$ 19,99/mês)Cloud a partir de 20 €/mês; self-host sem licençaFree 10 mil msg; Starter US$ 250/mês
Onde o fluxo viveNo seu códigoNo seu códigoNa conta ZapierNo n8nNo Knock
Filtro de ruído embutidoSimVocê constróiVocê configuraVocê configuraNão se aplica
Retry e assinatura na entregaVia Webhooks EngineVocê constróiInternoInternoInterno
Preferências por usuário e agrupamentoNãoNãoNãoNãoSim, é o forte deles
Block Kit, modais, Socket ModeNão (§15)SimParcialParcialParcial
MaturidadeAlpha (§15)ProduçãoProduçãoProduçãoProdução

Preços consultados nas páginas públicas em 2026-08-16: Zapier, n8n, Knock, Courier, Make. Variam por região, plano e negociação.

Nossos diferenciais

  1. O filtro roda antes da entrega, não depois. shouldForward é uma função pura, testada, que decide no processo do building block. Uma ferramenta de automação recebe tudo e filtra depois — e cobra por tudo que recebeu. Em workspace grande, essa diferença é a viabilidade do projeto.
  2. O canal reaproveita a infraestrutura de entrega dos outros canais. O dispatcher do Slack é o mesmo padrão do WPP e do WPP Business, sobre o mesmo Webhooks Engine. Quem já consome WhatsApp consome Slack com o mesmo verificador de assinatura e a mesma semântica de retry.
  3. A mensagem sai com identidade humana. O envio usa o token do usuário que instalou o app, com as_user: true. Em canal operacional, mensagem de bot anônimo é ignorada e mensagem com rosto é lida.
  4. O token do workspace fica na plataforma do cliente, cifrado. AES-256-GCM sob chave mestra própria do módulo, no banco da própria Catalisa, sem terceiro no meio do caminho entre o Slack e o produto.

Quando escolher o concorrente.

flowchart TD
  Q1{"O problema é notificação de saída<br/>orquestrada — preferências, agrupamento,<br/>digest, fallback entre canais?"}
  Q1 -->|Sim| K["Knock ou Courier.<br/>Este BB não resolve — não é o que ele é"]
  Q1 -->|Não| Q2{"Quem precisa da integração<br/>é time de negócio, não de engenharia?"}
  Q2 -->|Sim| Z["Zapier. Entrega numa tarde o que<br/>aqui exige endpoint HTTP e deploy"]
  Q2 -->|Não| Q3{"Precisa de Block Kit, modais,<br/>slash commands ou Socket Mode?"}
  Q3 -->|Sim| SP["Slack Platform direta.<br/>Nada disso está aqui — ver §15"]
  Q3 -->|Não| Q4{"Você atende um workspace só,<br/>o seu?"}
  Q4 -->|Sim| DIR["Construa direto. Mais simples que<br/>adotar um building block em alpha"]
  Q4 -->|Não| CAT["Catalisa Slack: ingestão bidirecional,<br/>muitos workspaces de muitos clientes,<br/>na plataforma que já entrega WhatsApp"]

Em prosa: se o problema é notificação de saída bem orquestrada — preferências por usuário, agrupamento, digest, múltiplos canais com fallback —, o Knock e o Courier resolvem hoje e este building block não resolve nunca, porque não é o que ele é. Se o time que precisa da integração é de negócio e não de engenharia, o Zapier entrega em uma tarde o que aqui exige um endpoint HTTP e um deploy. Se você precisa de Block Kit, modais, slash commands ou Socket Mode, a Slack Platform direta é o único caminho — nada disso está aqui (§15). E se você atende um workspace, o seu, construir direto é mais simples que adotar um building block em alpha. O Slack da Catalisa ganha num caso específico: ingestão bidirecional, muitos workspaces de muitos clientes, dentro de uma plataforma que já entrega WhatsApp pelo mesmo caminho.


06

Modelo de cobrança e ROI

negócio

Unidade de cobrança. Precificação em definição. O building block está em alpha e não tem preço fechado. Não há valor a informar, e não vamos inventar um.

O que dispara custo.

DriverPor que pesa
Workspaces conectadosCada conexão é um token cifrado para guardar e um conjunto de dispatchers para manter
Mensagens ingeridasCada evento aceito pelo filtro vira uma entrega pelo Webhooks Engine, com retry se falhar
Mensagens enviadasCada POST /messages/send é uma chamada chat.postMessage, sujeita ao limite da Slack

Comparação de custo — cenário: 20 clientes com 20 workspaces, cerca de 30 mil mensagens ingeridas e 10 mil enviadas por mês, depois do filtro de ruído.

Catalisa SlackZapiern8n CloudKnock
Base de cálculoEm definiçãoPor tarefaPor execução de workflowPor mensagem
Ordem de grandeza mensalA definir40 mil tarefas/mês fica bem acima da faixa de entrada de US$ 19,9940 mil execuções exigem o plano Business, na casa de 667 €/mês40 mil mensagens cabem no Starter de US$ 250/mês
Ingestão bidirecionalSimSimSimNão é o foco
Filtro antes de contarSimNão — conta o que entraNãoNão se aplica
App Slack distribuídoVocê ainda precisa aprovar o seu (§15)Zapier usa o app delesVocê constróiKnock usa o app deles

Estimativas montadas sobre as tabelas públicas consultadas em 2026-08-16 e citadas na §5. São ordens de grandeza para orientar conversa, não proposta comercial. Os valores da Catalisa não estão definidos.

ROI. O retorno não está na linha de licença — para volume baixo, quase tudo é barato. Está em duas coisas.

Fonte do retornoO que aconteceOrdem de grandeza
Trabalho de plataforma que não é feitoApp distribuído, OAuth por workspace, cifra de token, verificação de assinatura, ack de três segundos e filtroSemanas de engenharia que não produzem valor visível ao usuário
Efeito do filtro sobre quem cobra por eventoContar tudo o que o workspace produz versus contar só o que tem endereçoUma ordem de grandeza, em workspace grande

A primeira é o trabalho de plataforma que não é feito: app distribuído, OAuth por workspace, cifra de token, verificação de assinatura, ack de três segundos e filtro somam semanas que não produzem valor visível ao usuário. A segunda é o efeito do filtro no modelo de custo de qualquer ferramenta que cobre por evento: em workspace grande, a diferença entre contar tudo e contar o que tem endereço é de uma ordem de grandeza.


07

Arquitetura

Camadas e caminho da requisição

flowchart TD
  HTTP["HTTP"] --> APP

  subgraph APP["Hono app — basePath /slack"]
    R1["/api/v1/slack/connections<br/>connectionRouter · 3 rotas"]
    R2["/api/v1/slack/dispatchers<br/>dispatcherRouter · 2 rotas"]
    R3["/api/v1/slack/messages<br/>messagesRouter · 1 rota"]
    R4["/api/v1/slack/channels<br/>channelsRouter · 1 rota"]
    R5["/api/v1/slack/events<br/>slackEventsRouter · 1 rota — chamada pela Slack"]
    R6["/health e /connected<br/>página de retorno pós-OAuth"]
  end

  APP --> SVC

  subgraph SVC["services/"]
    S1["SlackConnectionService<br/>OAuth v2, state cifrado, upsert"]
    S2["SlackEventsService<br/>assinatura, filtro de ruído, fan-out"]
    S3["SlackSendService<br/>chat.postMessage como o usuário"]
    S4["SlackChannelsService<br/>conversations.list"]
    S5["SlackDispatcherService<br/>cria assinatura no Webhooks Engine"]
  end

  SVC --> REPO["repositories/ Prisma<br/>schema slack · 2 modelos"]
  SVC --> WC["@slack/web-api WebClient<br/>api.slack.com"]

O ciclo completo — autorizar, cifrar, ligar a entrega, receber push

Este é o fluxo ponta a ponta, das quatro etapas que o integrador percorre uma vez por workspace. O state do OAuth e o token de usuário usam o mesmo esquema AES-256-GCM do building block Calendar — iv:authTag:ciphertext em hex, IV aleatório de 12 bytes por operação — só que sob chave mestra própria do módulo.

sequenceDiagram
  autonumber
  participant U as "Usuário do cliente"
  participant APP as "Seu produto"
  participant BB as "BB Slack"
  participant DB as "PostgreSQL — schema slack"
  participant SL as "Slack Platform"
  participant WE as "Webhooks Engine"

  rect rgb(238, 244, 255)
    note over U,SL: "1. Autorizar"
    APP->>BB: "POST /connections/connect"
    BB->>BB: "encryptCredentials — organizationId, userId, redirectUrl"
    BB-->>APP: "authorizationUrl com state cifrado e user_scope"
    APP-->>U: "Redireciona para slack.com/oauth/v2/authorize"
    U->>SL: "Autoriza o app no workspace"
    SL-->>BB: "GET /connections/callback — code e state"
  end

  rect rgb(240, 248, 240)
    note over BB,DB: "2. Guardar a credencial cifrada"
    BB->>BB: "decryptCredentials do state — falha vira Invalid callback state"
    BB->>SL: "oauth.v2.access — troca code por token de usuário"
    SL-->>BB: "authed_user.access_token, team.id, scopes"
    BB->>BB: "encryptCredentials — AES-256-GCM, IV de 12 bytes"
    BB->>DB: "upsert em slack_connections — chave org + teamId + slackUserId"
    BB-->>U: "Redireciona para o redirectUrl do state"
  end

  rect rgb(255, 248, 236)
    note over APP,WE: "3. Ligar a entrega — o equivalente ao sincronizar"
    APP->>BB: "POST /dispatchers — endpointUrl e watchedChannels"
    BB->>WE: "createSubscription — filtra slack.v1.message.received"
    WE-->>BB: "subscriptionId"
    BB-->>APP: "secret de 32 bytes, entregue uma única vez"
  end

  rect rgb(252, 240, 244)
    note over U,APP: "4. Receber o push da Slack"
    U->>SL: "Escreve uma DM ou menciona o usuário conectado"
    SL->>BB: "POST /events — x-slack-signature e timestamp"
    BB->>BB: "Lê o corpo bruto e verifica HMAC-SHA256 em tempo constante"
    BB-->>SL: "200 ok em menos de 3 segundos"
    BB->>DB: "Resolve conexão por teamId e authorizations[0]"
    BB->>BB: "shouldForward — filtro de ruído"
    BB->>WE: "emitSlackPublic — slack.v1.message.received"
    WE->>APP: "Entrega assinada, com retry"
  end

Atenção. Não existe passo de sincronização por varredura, como num BB de calendário: a Slack empurra o evento. O que a etapa 3 liga é o caminho de saída da entrega, não uma sincronização periódica. E o BB não renova token — ver §15.

Caminho de uma mensagem que entra, em detalhe

flowchart TD
  U["Usuário escreve no Slack"] --> P["POST /slack/api/v1/slack/events<br/>rota pública — sem token da plataforma"]
  P --> A1["1. Lê o corpo BRUTO como texto,<br/>antes de qualquer parse"]
  A1 --> A2{"2. x-slack-signature confere<br/>contra SLACK_SIGNING_SECRET?<br/>HMAC-SHA256 de v0:timestamp:corpo<br/>timestamp dentro de 5 minutos?"}
  A2 -->|Não| E401["401 invalid signature"]
  A2 -->|Sim| A3{"3. O corpo é JSON válido?"}
  A3 -->|Não| E400["400 invalid json"]
  A3 -->|Sim| A4{"4. type é url_verification?"}
  A4 -->|Sim| CH["200 com o challenge em texto puro"]
  A4 -->|Não| A5{"5. event_callback de<br/>message ou app_mention?"}
  A5 -->|Sim| FF["Dispara ingestEvent SEM aguardar"]
  A5 -->|Não| ACK
  FF --> ACK["6. Responde 200 imediatamente<br/>janela de 3s da Slack"]
  FF --> ING

  subgraph ING["SlackEventsService.ingestEvent"]
    I1["1. Conexão por team_id e authorizations[0]"]
    I2["2. Dispatchers ACTIVE da conexão"]
    I3["3. União dos watchedChannels"]
    I4["4. shouldForward — descarta bot_message,<br/>message_changed, message_deleted,<br/>channel_join e channel_leave;<br/>passa im, mpim, menção e canal observado"]
    I5["5. emitSlackPublic<br/>slack.v1.message.received"]
    I1 --> I2 --> I3 --> I4 --> I5
  end

  ING --> WE["Webhooks Engine<br/>assinatura · retry · log"]
  WE --> CLI["Endpoint do cliente"]

Decisões não óbvias.

  • A assinatura é verificada em toda requisição, inclusive no url_verification. Muita implementação libera o desafio de verificação antes de checar assinatura, porque é o primeiro POST que a Slack manda. Aqui não: sem SLACK_SIGNING_SECRET configurado, ou com assinatura inválida, a resposta é 401 mesmo para o desafio. Custa um passo a mais na configuração e fecha um caminho em que qualquer um posta na sua rota pública.
  • O corpo é lido como texto antes de qualquer parse. await c.req.text() vem primeiro, e o HMAC é calculado sobre esses bytes. Fazer o parse e reserializar muda espaçamento e ordem de chave, e a assinatura deixa de bater — de forma silenciosa e intermitente, que é o pior modo de falhar.
  • A comparação usa timingSafeEqual, com verificação de comprimento antes. Comparação de string com === vaza informação por tempo de execução. E o timingSafeEqual do Node lança exceção com buffers de tamanhos diferentes, por isso o comprimento é conferido antes.
  • Tolerância de 5 minutos no timestamp. Requisição capturada e reenviada depois disso é rejeitada. Cinco minutos é a folga que absorve relógio dessincronizado sem abrir janela de repetição relevante.
  • A ingestão é disparada e esquecida. O void service.ingestEvent(...) sai do caminho da resposta de propósito: a Slack exige 2xx em três segundos e reenvia três vezes se não receber (Slack — Events API, 2026-08-16). Processar antes de responder produz mensagem duplicada sob carga. O preço é que uma falha na ingestão vira log de warn, não erro HTTP — o cliente do Slack não fica sabendo.
  • Erro de ingestão é sempre não fatal. Conexão não encontrada, dispatcher ausente ou falha de publicação não derrubam a resposta. Evento de workspace que ninguém conectou é ignorado em silêncio, o que é o comportamento certo para uma rota pública.
  • O app OAuth é global, não por organização. SLACK_CLIENT_ID, SLACK_CLIENT_SECRET e SLACK_CALLBACK_URL vêm do ambiente e são registrados uma vez no container. É uma diferença deliberada em relação ao Calendar, que guarda credenciais OAuth por tenant: no Slack existe um app da plataforma, e cada cliente instala esse app no workspace dele. O trade-off é que a tela de consentimento leva a marca do app da plataforma, não a do cliente.
  • O envio usa as_user: true. A mensagem sai com a identidade do usuário que instalou, não de um bot. É intencional — o token pedido é user_scope, não bot token — e tem consequência: o app precisa do escopo chat:write de usuário, e a mensagem conta como escrita por aquela pessoa.
  • O filtro considera a união dos watchedChannels de todos os dispatchers ativos. Se um dispatcher observa #a e outro observa #b, a mensagem de #a passa pelo filtro e é publicada uma vez; a separação por dispatcher acontece no Webhooks Engine, pelos filtros da assinatura.

Monolito vs. standalone. Em monolito, o Slack é resolvido pelo container TypeDI e o SlackDispatcherService fala com o Webhooks Engine por chamada local. Em standalone — o modo de produção — sobe como serviço próprio (porta 3029 no compose local) e o dispatcher usa a facade remota: sem MODULE_WEBHOOKS_ENGINE_URL configurado, POST /dispatchers responde 503. Está anotado no próprio stack de deploy.


08

Conceitos e modelo de dados

Glossário

TermoSignifica
ConnectionO vínculo entre um usuário da plataforma, uma organização e um workspace do Slack. Guarda o token cifrado. Único por (organizationId, teamId, slackUserId).
TeamO workspace do Slack. O teamId (T…) é como a Slack o identifica em todo evento.
Slack user idO identificador do usuário no workspace (U…). Quem instalou o app e em nome de quem as mensagens saem.
DispatcherA regra de encaminhamento: para qual endpoint entregar e quais canais observar. Cria uma assinatura no Webhooks Engine.
Watched channelsLista de channelId observados. Só afeta canais comuns — DM e menção passam sempre.
Signing secretSegredo do app Slack usado para verificar a assinatura de cada requisição recebida. Global, do ambiente.
Envelope cifradoJSON {accessToken, refreshToken} cifrado com AES-256-GCM e gravado como uma string.
Evento públicoslack.v1.message.received, publicado no barramento e entregue pelo Webhooks Engine.

Modelo de dados — schema slack no PostgreSQL.

Modelo PrismaTabelaPropósitoCampos-chave
SlackConnectionslack.slack_connectionsWorkspace conectado por um usuárioteamId, teamName, slackUserId, accessToken (envelope cifrado), scopes[], status, único (organizationId, teamId, slackUserId)
SlackDispatcherslack.slack_dispatchersRegra de encaminhamentoendpointUrl, customHeaders, watchedChannels[], subscriptionId, status

São dois modelos e um relacionamento — a conexão é o dono, o dispatcher é a regra pendurada nela:

erDiagram
  SlackConnection ||--o{ SlackDispatcher : "tem"
  SlackConnection {
    uuid id PK
    uuid organizationId "do JWT, nunca do corpo"
    uuid userId "usuário da plataforma"
    string teamId "workspace T..."
    string teamName
    string slackUserId "usuário U... que instalou"
    string accessToken "envelope AES-256-GCM"
    string_array scopes
    enum status "só ACTIVE é atribuído hoje"
  }
  SlackDispatcher {
    uuid id PK
    uuid connectionId FK
    uuid organizationId
    string endpointUrl "para onde o Webhooks Engine entrega"
    json customHeaders "inclui x-webhook-secret"
    string_array watchedChannels "só afeta canal comum"
    string subscriptionId "assinatura no Webhooks Engine"
    enum status "só ACTIVE é atribuído hoje"
  }

Enumerações

EnumValores
SlackConnectionStatusACTIVE e demais estados do schema. Só ACTIVE é atribuído hoje
SlackDispatcherStatusACTIVE e demais estados do schema. Só ACTIVE é atribuído hoje

Escopos de usuário solicitados

EscopoPara quê
im:history · im:readLer e listar DMs
mpim:history · mpim:readLer e listar grupos de DM
channels:history · channels:readLer e listar canais públicos dos quais o usuário participa
groups:history · groups:readLer e listar canais privados dos quais o usuário participa
users:readResolver nome e perfil
chat:writePostar como o usuário

O token de usuário só alcança as DMs daquele usuário e os canais em que ele já está. Não há caminho para ler conversa privada de terceiros.

Decisão do filtro de ruído

As quatro perguntas são avaliadas nessa ordem, e a primeira que responde decide. É uma função pura, shouldForward, testada isoladamente:

flowchart TD
  EV["Evento chega"] --> Q1{"subtype está em bot_message,<br/>message_changed, message_deleted,<br/>channel_join ou channel_leave?"}
  Q1 -->|Sim| DROP1["DESCARTA"]
  Q1 -->|Não| Q2{"channelType é im ou mpim?"}
  Q2 -->|Sim| OK1["ENTREGA — DM ou grupo de DM"]
  Q2 -->|Não| Q3{"O texto contém a menção<br/>ao slackUserId da conexão?"}
  Q3 -->|Sim| OK2["ENTREGA — menção"]
  Q3 -->|Não| Q4{"channelId está na união dos<br/>watchedChannels dos dispatchers ACTIVE?"}
  Q4 -->|Sim| OK3["ENTREGA — canal observado"]
  Q4 -->|Não| DROP2["DESCARTA"]

Ciclo de vida da conexão

O caminho até ACTIVE passa por três etapas, e não há saída dele pela API hoje:

stateDiagram-v2
  [*] --> Solicitada : "POST /connections/connect<br/>gera state cifrado com<br/>organizationId, userId e redirectUrl"
  Solicitada --> Consentida : "usuário autoriza em<br/>slack.com/oauth/v2/authorize"
  Consentida --> ACTIVE : "GET /connections/callback (rota pública)<br/>decifra o state, oauth.v2.access,<br/>cifra o token, faz upsert"
  Solicitada --> Falha : "state adulterado ou<br/>chave mestra trocada"
  Consentida --> Falha : "resposta do OAuth sem<br/>authed_user.access_token"
  Falha --> [*] : "400 Invalid callback state"
  ACTIVE --> ACTIVE : "reconectar faz upsert na mesma<br/>linha e limpa deletedAt"
  note right of ACTIVE
    Não há endpoint de desconexão — ver §15.
    Não há renovação de token — ver §15.
    Os demais valores do enum
    SlackConnectionStatus existem no schema
    mas nenhum caminho de código os atribui.
  end note

09

Referência da API

Prefixo real: /slack/api/v1/slack. O nome do módulo aparece duas vezes porque o app usa basePath('/slack') e os routers são montados sob /api/v1/slack. Em standalone, a base é https://slack.stg.catalisa.app.

Salvo indicação de rota pública, toda rota exige authMiddleware, o requirePermission(...) da tabela e requireOrganization — token sem organizationId recebe 403 com Organization context required.

Conexões — /slack/api/v1/slack/connections

MétodoRotaDescriçãoPermissão
POST/slack/api/v1/slack/connections/connectDevolve a URL de autorização do SlackSLACK_WRITE
GET/slack/api/v1/slack/connections/callbackCallback OAuth. Rota pública — sem authMiddlewarePública
GET/slack/api/v1/slack/connectionsLista as conexões do usuário do tokenSLACK_READ

Dispatchers — /slack/api/v1/slack/dispatchers

MétodoRotaDescriçãoPermissão
POST/slack/api/v1/slack/dispatchersCria dispatcher e a assinatura no Webhooks Engine. 201SLACK_WRITE
PATCH/slack/api/v1/slack/dispatchers/:idAtualiza apenas watchedChannelsSLACK_WRITE

Mensagens e canais

MétodoRotaDescriçãoPermissão
POST/slack/api/v1/slack/messages/sendEnvia mensagem como o usuário conectadoSLACK_WRITE
GET/slack/api/v1/slack/channels?teamId=&slackUserId=Lista canais, DMs e grupos da conexãoSLACK_READ

Eventos do Slack — rota pública

MétodoRotaDescriçãoPermissão
POST/slack/api/v1/slack/eventsRecebe url_verification e event_callback da SlackPública, autenticada por x-slack-signature

Saúde e retorno de OAuth

MétodoRotaDescrição
GET/slack/healthSonda de vida e versão do build
GET/slack/connectedPágina HTML estática de confirmação, para usar como redirectUrl pós-OAuth

POST /slack/api/v1/slack/connections/connect

Request

json
{ "redirectUrl": "https://app.suaempresa.com.br/integracoes/slack/pronto" }
{ "redirectUrl": "https://app.suaempresa.com.br/integracoes/slack/pronto" }
CampoTipoObrigatórioDescrição
redirectUrlstring (URL)SimPara onde o usuário volta depois do callback. Viaja cifrado no state

Resposta 200

json
{
  "authorizationUrl": "https://slack.com/oauth/v2/authorize?client_id=...&user_scope=im%3Ahistory%2C...&state=..."
}
{
  "authorizationUrl": "https://slack.com/oauth/v2/authorize?client_id=...&user_scope=im%3Ahistory%2C...&state=..."
}

Note o user_scope: o fluxo pede token de usuário, não de bot.

Erros

StatusQuando
400redirectUrl ausente ou não é URL válida
403Sem SLACK_WRITE ou sem organização no token
500SLACK_CREDENTIAL_MASTER_KEY ausente ou malformada — a cifra do state falha

POST /slack/api/v1/slack/dispatchers

Cria a regra de encaminhamento e, junto com ela, a assinatura no Webhooks Engine.

Request

json
{
  "connectionId": "8f14e45f-ceea-467a-9e1a-2b3c4d5e6f70",
  "teamId": "T01ABCDEF",
  "slackUserId": "U01ABCDEF",
  "endpointUrl": "https://api.suaempresa.com.br/hooks/slack",
  "watchedChannels": ["C01OPERACAO", "C02SUPORTE"],
  "customHeaders": { "X-Origem": "slack" }
}
{
  "connectionId": "8f14e45f-ceea-467a-9e1a-2b3c4d5e6f70",
  "teamId": "T01ABCDEF",
  "slackUserId": "U01ABCDEF",
  "endpointUrl": "https://api.suaempresa.com.br/hooks/slack",
  "watchedChannels": ["C01OPERACAO", "C02SUPORTE"],
  "customHeaders": { "X-Origem": "slack" }
}
CampoTipoObrigatórioDescrição
connectionIdstring (UUID)SimConexão à qual o dispatcher pertence
teamIdstringSimWorkspace. Compõe o nome da assinatura
slackUserIdstringSimUsuário conectado. Compõe o nome da assinatura
endpointUrlstring (URL)SimPara onde o Webhooks Engine entrega
watchedChannelsstring[]NãoCanais comuns observados. Padrão [] — só DM e menção passam
customHeadersobjectNãoCabeçalhos extras em cada entrega

Resposta 201

json
{
  "data": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "secret": "c0ffee…",
    "subscriptionId": "aaaa-bbbb-cccc-dddd"
  }
}
{
  "data": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "secret": "c0ffee…",
    "subscriptionId": "aaaa-bbbb-cccc-dddd"
  }
}

O secret de 32 bytes é gerado aqui e enviado ao Webhooks Engine como o cabeçalho x-webhook-secret de toda entrega. Ele aparece uma vez, nesta resposta. Não há endpoint para recuperá-lo.

A assinatura criada filtra slack.v1.message.received e slack.v1.message.sent, com timeout de 10 s e até 3 tentativas em backoff exponencial (1 s, 2 s, 4 s). O segundo evento é declarado mas não é emitido hoje — ver §15.

Erros

StatusQuando
400Corpo reprovado no Zod
403Sem SLACK_WRITE ou sem organização
503Em standalone, MODULE_WEBHOOKS_ENGINE_URL não configurado

POST /slack/api/v1/slack/messages/send

Request

json
{
  "teamId": "T01ABCDEF",
  "slackUserId": "U01ABCDEF",
  "channelId": "C01OPERACAO",
  "text": "Entrega 4471 fora da janela. Quer que eu abra a tratativa?"
}
{
  "teamId": "T01ABCDEF",
  "slackUserId": "U01ABCDEF",
  "channelId": "C01OPERACAO",
  "text": "Entrega 4471 fora da janela. Quer que eu abra a tratativa?"
}
CampoTipoObrigatórioRestrições
teamIdstringSimMínimo 1 caractere
slackUserIdstringSimMínimo 1 caractere
channelIdstringSimID do canal, DM ou grupo (C…, D…, G…)
textstringSim1 a 40.000 caracteres

A tripla (organizationId do token, teamId, slackUserId) resolve a conexão e o token — o organizationId nunca vem do corpo.

Resposta 200

json
{ "data": { "correlationId": "1756742400.123456" } }
{ "data": { "correlationId": "1756742400.123456" } }

O correlationId é o ts da mensagem no Slack. É o que identifica a mensagem e o que se usa como thread_ts para responder em thread.

Erros

StatusQuando
400Corpo reprovado no Zod, ou a Slack devolveu erro (Slack error: channel_not_found, not_in_channel, ...)
403Sem SLACK_WRITE ou sem organização
404Não há conexão para essa tripla, ou ela foi excluída
500Falha na chamada à Slack, ou o token não decifra

POST /slack/api/v1/slack/events

Rota pública, chamada pela Slack. Configure-a como Request URL em Event Subscriptions.

Cabeçalhos exigidos

CabeçalhoUso
x-slack-request-timestampÉpoca em segundos. Desvio maior que 5 minutos é recusado
x-slack-signaturev0= seguido do HMAC-SHA256 de v0:{timestamp}:{corpo bruto} com o SLACK_SIGNING_SECRET

Respostas

SituaçãoResposta
Assinatura inválida, ausente, fora da janela, ou SLACK_SIGNING_SECRET não configurado401 {"error":"invalid signature"}
Corpo não é JSON400 {"error":"invalid json"}
type: url_verification200 com o challenge em texto puro
type: url_verification sem challenge string400 {"error":"missing challenge"}
event_callback de message ou app_mention200 {"ok":true} — ingestão dispara em paralelo
Qualquer outro tipo200 {"ok":true}

Payload publicado quando o evento passa no filtro — tipo slack.v1.message.received:

json
{
  "originalPayload": {
    "teamId": "T01ABCDEF",
    "slackUserId": "U01ABCDEF",
    "channelId": "C01OPERACAO",
    "channelType": "channel",
    "ts": "1756742400.123456",
    "threadTs": null,
    "senderId": "U09XYZ",
    "senderName": null,
    "text": "qual o status da proposta 4471?",
    "subtype": null,
    "files": []
  }
}
{
  "originalPayload": {
    "teamId": "T01ABCDEF",
    "slackUserId": "U01ABCDEF",
    "channelId": "C01OPERACAO",
    "channelType": "channel",
    "ts": "1756742400.123456",
    "threadTs": null,
    "senderId": "U09XYZ",
    "senderName": null,
    "text": "qual o status da proposta 4471?",
    "subtype": null,
    "files": []
  }
}

10

Início rápido

Do zero à primeira mensagem trafegando. Os comandos não foram executados ao escrever esta documentação — conferem com o código e os schemas Zod, mas trate-os como referência.

Pré-requisito: o app Slack precisa existir e estar configurado. O passo a passo completo, com manifesto pronto, está em `docs/slack/SETUP.md`. Sem SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_SIGNING_SECRET, SLACK_CALLBACK_URL e SLACK_CREDENTIAL_MASTER_KEY, nada abaixo funciona.

São oito passos, e o oitavo é o que fecha o ciclo nos dois sentidos:

flowchart LR
  P1["1. Autenticar<br/>no IAM"] --> P2["2. Conferir que<br/>/events dá 401"]
  P2 --> P3["3. Conectar<br/>o workspace"]
  P3 --> P4["4. Verificar<br/>a conexão"]
  P4 --> P5["5. Descobrir<br/>os canais"]
  P5 --> P6["6. Criar<br/>o dispatcher"]
  P6 --> P7["7. Enviar<br/>uma mensagem"]
  P7 --> P8["8. Fechar o ciclo —<br/>escrever uma DM"]

1. Autenticar no IAM

bash
TOKEN=$(curl -s -X POST https://iam.stg.catalisa.app/iam/api/v1/users/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@catalisa.app","password":"password123",
       "organizationId":"b0000000-0000-0000-0000-000000000001"}' | jq -r .accessToken)

BASE=https://slack.stg.catalisa.app/slack/api/v1/slack
TOKEN=$(curl -s -X POST https://iam.stg.catalisa.app/iam/api/v1/users/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@catalisa.app","password":"password123",
       "organizationId":"b0000000-0000-0000-0000-000000000001"}' | jq -r .accessToken)

BASE=https://slack.stg.catalisa.app/slack/api/v1/slack

Confira que o token chegou antes de seguir — echo $TOKEN precisa imprimir um JWT, não null:

texto
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI…
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI…

2. Confirmar que a rota de eventos está protegida

bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST "$BASE/events" \
  -H "Content-Type: application/json" -d '{"type":"url_verification","challenge":"x"}'
curl -s -o /dev/null -w "%{http_code}\n" -X POST "$BASE/events" \
  -H "Content-Type: application/json" -d '{"type":"url_verification","challenge":"x"}'

Deve responder 401. Se responder 200, o SLACK_SIGNING_SECRET não está fazendo o que deveria — pare e corrija antes de seguir.

3. Conectar o workspace

bash
curl -s -X POST "$BASE/connections/connect" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"redirectUrl":"https://slack.stg.catalisa.app/slack/connected"}' \
  | jq -r .authorizationUrl
curl -s -X POST "$BASE/connections/connect" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"redirectUrl":"https://slack.stg.catalisa.app/slack/connected"}' \
  | jq -r .authorizationUrl
texto
https://slack.com/oauth/v2/authorize?client_id=…&user_scope=im%3Ahistory%2C…&redirect_uri=…&state=…
https://slack.com/oauth/v2/authorize?client_id=…&user_scope=im%3Ahistory%2C…&redirect_uri=…&state=…

Abra a URL, autorize no workspace, e o callback grava a conexão e redireciona.

4. Verificar a conexão

bash
curl -s "$BASE/connections" -H "Authorization: Bearer $TOKEN" | jq
curl -s "$BASE/connections" -H "Authorization: Bearer $TOKEN" | jq
json
{
  "data": [
    { "id": "8f14e45f-…", "teamId": "T01ABCDEF", "teamName": "Acme",
      "slackUserId": "U01ABCDEF", "status": "ACTIVE" }
  ]
}
{
  "data": [
    { "id": "8f14e45f-…", "teamId": "T01ABCDEF", "teamName": "Acme",
      "slackUserId": "U01ABCDEF", "status": "ACTIVE" }
  ]
}

Guarde id, teamId e slackUserId — os três aparecem nas chamadas seguintes.

5. Descobrir os canais

bash
curl -s -G "$BASE/channels" -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "teamId=T01ABCDEF" \
  --data-urlencode "slackUserId=U01ABCDEF" | jq
curl -s -G "$BASE/channels" -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "teamId=T01ABCDEF" \
  --data-urlencode "slackUserId=U01ABCDEF" | jq
json
{ "data": [ { "id": "C01OPERACAO", "name": "operacao-sp", "isPrivate": false } ] }
{ "data": [ { "id": "C01OPERACAO", "name": "operacao-sp", "isPrivate": false } ] }

6. Criar o dispatcher

bash
curl -s -X POST "$BASE/dispatchers" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "connectionId": "8f14e45f-…",
    "teamId": "T01ABCDEF",
    "slackUserId": "U01ABCDEF",
    "endpointUrl": "https://api.suaempresa.com.br/hooks/slack",
    "watchedChannels": ["C01OPERACAO"]
  }' | jq
curl -s -X POST "$BASE/dispatchers" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "connectionId": "8f14e45f-…",
    "teamId": "T01ABCDEF",
    "slackUserId": "U01ABCDEF",
    "endpointUrl": "https://api.suaempresa.com.br/hooks/slack",
    "watchedChannels": ["C01OPERACAO"]
  }' | jq
json
{
  "data": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "secret": "c0ffee…",
    "subscriptionId": "aaaa-bbbb-cccc-dddd"
  }
}
{
  "data": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "secret": "c0ffee…",
    "subscriptionId": "aaaa-bbbb-cccc-dddd"
  }
}

Anote o secret — ele não volta nunca mais.

7. Enviar uma mensagem

bash
curl -s -X POST "$BASE/messages/send" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"teamId":"T01ABCDEF","slackUserId":"U01ABCDEF",
       "channelId":"C01OPERACAO","text":"Integração ativa."}' | jq
curl -s -X POST "$BASE/messages/send" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"teamId":"T01ABCDEF","slackUserId":"U01ABCDEF",
       "channelId":"C01OPERACAO","text":"Integração ativa."}' | jq
json
{ "data": { "correlationId": "1756742400.123456" } }
{ "data": { "correlationId": "1756742400.123456" } }

8. Fechar o ciclo

Escreva uma DM para o usuário conectado, ou mencione-o em #operacao-sp. Seu endpoint recebe slack.v1.message.received em segundos.

Credenciais de staging. Nunca cole segredo de produção em documentação ou script — ver AMBIENTES.md.


11

Receitas

Verificar a assinatura da entrega no seu endpoint

O Webhooks Engine assina a entrega, e o dispatcher acrescenta o x-webhook-secret gerado na criação. Confira os dois, e confira o segredo em tempo constante.

flowchart LR
  D["Entrega chega<br/>no seu endpoint"] --> V1{"Assinatura RSA do<br/>Webhooks Engine confere?"}
  V1 -->|Não| R1["401 — defesa principal"]
  V1 -->|Sim| V2{"x-webhook-secret confere<br/>em tempo constante?"}
  V2 -->|Não| R2["401 — segunda camada"]
  V2 -->|Sim| ACK["Responda 200 primeiro"]
  ACK --> FILA["Só então enfileire<br/>o trabalho"]
javascript
import { timingSafeEqual } from 'node:crypto'

app.post('/hooks/slack', express.raw({ type: 'application/json' }), (req, res) => {
  const recebido = Buffer.from(req.header('x-webhook-secret') ?? '')
  const esperado = Buffer.from(process.env.SLACK_DISPATCHER_SECRET)

  if (recebido.length !== esperado.length || !timingSafeEqual(recebido, esperado)) {
    return res.sendStatus(401)
  }

  // Responda rápido. Enfileire o trabalho.
  res.sendStatus(200)

  const { originalPayload } = req.body
  fila.add({ canal: 'slack', ...originalPayload })
})
import { timingSafeEqual } from 'node:crypto'

app.post('/hooks/slack', express.raw({ type: 'application/json' }), (req, res) => {
  const recebido = Buffer.from(req.header('x-webhook-secret') ?? '')
  const esperado = Buffer.from(process.env.SLACK_DISPATCHER_SECRET)

  if (recebido.length !== esperado.length || !timingSafeEqual(recebido, esperado)) {
    return res.sendStatus(401)
  }

  // Responda rápido. Enfileire o trabalho.
  res.sendStatus(200)

  const { originalPayload } = req.body
  fila.add({ canal: 'slack', ...originalPayload })
})

Armadilhas.

  • Responda antes de processar. O timeout do dispatcher é de 10 segundos; passar disso conta como falha e gera nova tentativa.
  • A entrega não é exatamente-uma-vez. Três tentativas em backoff significam que o mesmo ts pode chegar duas vezes. Use ts + channelId como chave de idempotência.
  • A assinatura RSA do Webhooks Engine é a defesa principal; o x-webhook-secret é a segunda camada. Ver Webhooks Engine.

Ajustar os canais observados sem recriar nada

bash
curl -s -X PATCH "$BASE/dispatchers/$DISPATCHER_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"watchedChannels":["C01OPERACAO","C02SUPORTE","C03LOGISTICA"]}'
curl -s -X PATCH "$BASE/dispatchers/$DISPATCHER_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"watchedChannels":["C01OPERACAO","C02SUPORTE","C03LOGISTICA"]}'
json
{ "ok": true }
{ "ok": true }

Armadilhas.

  • O corpo substitui a lista inteira. Não há acrescentar nem remover — mande sempre o conjunto completo.
  • Só watchedChannels é atualizável. Para mudar endpointUrl ou customHeaders, crie outro dispatcher; não há DELETE para o antigo (§15).
  • Tirar um canal da lista não para DM nem menção. Esses dois passam sempre, por desenho do filtro.
  • O usuário conectado precisa estar no canal. Canal fora da participação dele não gera evento, por mais que esteja na lista.

Responder dentro da thread da mensagem recebida

O POST /messages/send não aceita thread_ts hoje (§15). A alternativa que funciona:

bash
# O ts recebido no evento identifica a mensagem original
curl -s -X POST "$BASE/messages/send" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"teamId\":\"$TEAM\",\"slackUserId\":\"$USER\",
       \"channelId\":\"$CANAL\",
       \"text\":\"Sobre a proposta 4471: em análise desde ontem.\"}"
# O ts recebido no evento identifica a mensagem original
curl -s -X POST "$BASE/messages/send" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"teamId\":\"$TEAM\",\"slackUserId\":\"$USER\",
       \"channelId\":\"$CANAL\",
       \"text\":\"Sobre a proposta 4471: em análise desde ontem.\"}"

Armadilhas.

  • A resposta vai para o canal, não para a thread. Em canal movimentado, ela se perde do contexto. Se thread é requisito do seu produto, isso é bloqueante hoje.
  • O correlationId que volta é o ts da sua mensagem, e é o que você guarda para correlacionar resposta com pedido.
  • O limite de 40.000 caracteres é do schema; a Slack tem os limites de renderização dela. Mensagem muito longa é truncada ou rejeitada pelo lado deles.

Diagnosticar por que nada chega ao seu endpoint

Em ordem, do mais comum ao mais raro:

VerificaçãoComoO que significa falhar
A Request URL do app aponta para a rota certa?Console do Slack, Event SubscriptionsDeve ser {BASE_PUBLICA}/slack/api/v1/slack/events, com o slack duplicado
A Slack consegue verificar a URL?O console mostra Verified401 aqui quase sempre é SLACK_SIGNING_SECRET divergente entre o app e o ambiente
Os eventos de usuário estão assinados?Event Subscriptions → eventos de usuárioFaltam message.im, message.mpim, message.channels, message.groups, app_mention
A conexão existe?GET /connectionsSe a lista está vazia, o OAuth não concluiu
Há dispatcher ativo?Não há endpoint de listagem (§15) — consulte slack.slack_dispatchersSem dispatcher ACTIVE, ingestEvent sai antes de publicar
O canal está observado?PATCH /dispatchers/:idCanal comum fora de watchedChannels é descartado. Teste com DM, que passa sempre
A mensagem tem subtipo de ruído?—Mensagem editada, apagada, de bot ou de entrada/saída é descartada por desenho
O Webhooks Engine está alcançável?Log do serviçoEm standalone, sem MODULE_WEBHOOKS_ENGINE_URL a criação do dispatcher já teria dado 503

Atalho: teste sempre com DM primeiro. DM ignora watchedChannels e elimina metade das causas possíveis em uma tentativa.


12

Integração com outros building blocks

Building blockComo se relacionaObrigatório
IAMEmite o token com organizationId e as permissões SLACK_*Sim
Webhooks EngineEntrega os eventos com assinatura, retry e log. O dispatcher cria a assinaturaSim, para dispatcher
WPPMesmo padrão de dispatcher. Quem consome WhatsApp consome Slack com o mesmo verificadorNão
WPP BusinessIdem, pelo canal Meta Cloud APINão
CalendarPadrão irmão de OAuth por conexão, com credenciais cifradas — com uma diferença importante (abaixo)Não
Audit TrailRegistro de quem conectou workspace e criou dispatcherNão

O padrão de dispatcher da casa

Slack, WPP e WPP Business resolvem o mesmo problema: pegar evento de um canal de mensagem e entregar ao sistema do cliente com garantia. Os três usam o mesmo Webhooks Engine por baixo.

flowchart TD
  WA["WhatsApp<br/>Baileys"] --> BB1["BB wpp<br/>wpp.v1.*"]
  MC["WhatsApp<br/>Meta Cloud"] --> BB2["BB wpp-business<br/>wpp-biz.*"]
  SL["Slack"] --> BB3["BB slack<br/>slack.v1.*"]
  BB1 --> WE
  BB2 --> WE
  BB3 --> WE
  WE["Webhooks Engine<br/>assinatura RSA · retry ·<br/>log de entrega · filtros"] --> EP["Endpoint único do cliente"]

O argumento comercial: um consumidor, três canais. O cliente escreve o verificador de assinatura e o tratamento de idempotência uma vez.

Onde o Slack difere dos irmãos, e vale saber antes de integrar:

wpp / wpp-businessslack
Modos de dispatcherBASIC e ADVANCED, com condições JSONPathUm modo só
Filtro por padrão glob de eventoSimNão — um tipo de evento só
Listar, apagar e pausar dispatcherSimNão (§15)
Histórico de entregas por APISimNão (§15)
Retentativa manual de entregaSimNão (§15)
Namespacewpp.v1.* · wpp-biz.*slack.v1.*

A referência completa do padrão está em `docs/wpp/dispatchers.md`. O Slack é a versão mínima dele.

A diferença em relação ao Calendar.

flowchart TD
  subgraph CAL["BB Calendar — app por organização"]
    C1["CalendarProviderConfig<br/>client_id e client_secret<br/>cifrados por tenant"] --> C2["Tela de consentimento<br/>leva a marca do cliente"]
    C2 --> C3["Cada cliente passa pela<br/>verificação do Google sozinho"]
  end
  subgraph SLK["BB Slack — app da plataforma"]
    S1["SLACK_CLIENT_ID e SLACK_CLIENT_SECRET<br/>vindos do ambiente, uma vez"] --> S2["Tela de consentimento<br/>leva a marca da plataforma"]
    S2 --> S3["Aprovação do app é uma só<br/>e é gargalo compartilhado — §15"]
  end
  COM["Em comum: state cifrado e<br/>token AES-256-GCM no banco"] --> CAL
  COM --> SLK

Os dois fazem OAuth com state cifrado e token AES-256-GCM no banco, mas o app OAuth mora em lugares diferentes: o Calendar guarda client_id e client_secret por organização, então a tela de consentimento leva a marca do cliente; o Slack usa um app da plataforma, vindo do ambiente, que cada cliente instala no workspace dele. Vem do modelo da própria Slack, em que um app distribuído é instalado em muitos workspaces. A consequência prática: no Calendar cada cliente passa pela verificação do Google sozinho; no Slack a aprovação do app é uma só, da plataforma, e é gargalo compartilhado (§15).


13

Configuração e operação

Variáveis de ambiente

VariávelDescriçãoObrigatóriaPadrão
SLACK_CLIENT_IDClient ID do app SlackSim—
SLACK_CLIENT_SECRETClient Secret do app SlackSim—
SLACK_SIGNING_SECRETSigning Secret. Sem ele, POST /events recusa tudo com 401Sim—
SLACK_CALLBACK_URL{BASE_PUBLICA}/slack/api/v1/slack/connections/callback. Idêntico ao cadastrado no appSim—
SLACK_CREDENTIAL_MASTER_KEYChave AES-256-GCM. 64 caracteres hex (32 bytes). openssl rand -hex 32Sim—
MODULE_WEBHOOKS_ENGINE_URLURL interna do Webhooks Engine. Sem ela, POST /dispatchers dá 503 em standaloneEm standalone''
MODULE_SLACK_URLURL interna do serviçoEm standalone''
MODULE_SLACK_PORTPorta em standaloneNão3029
DATABASE_URLPostgreSQL, schema slackSim—
JWT_SECRETCompartilhado com o IAM, mínimo 44 caracteresSim—

Cuidado na primeira subida. SLACK_CLIENT_ID, SLACK_CLIENT_SECRET e SLACK_CALLBACK_URL são opcionais no schema de configuração e, quando ausentes, viram string vazia no container — o serviço sobe e só falha na primeira tentativa de conexão. A SLACK_CREDENTIAL_MASTER_KEY é lida direto de process.env, fora do schema de configuração, e valida o formato apenas no momento do uso. Confirme as cinco variáveis logo após o deploy chamando POST /connections/connect: se ela devolve uma authorizationUrl com o client_id certo, está tudo no lugar.

Dependências de infraestrutura

DependênciaPara quê
PostgreSQLSchema slack, 2 tabelas
IAMEmissão e validação do token
Webhooks EngineAssinatura, retry e log de entrega dos dispatchers
Slack PlatformOAuth, chat.postMessage, conversations.list, Events API

Limites e quotas

LimiteValorOrigem
Tamanho do texto enviado1 a 40.000 caracteresSchema Zod
Janela de ack do Events API3 segundos, com 3 reenvios (imediato, 1 min, 5 min)Slack — Events API, 2026-08-16
Tolerância do timestamp da assinatura5 minutosverifySlackSignature
chat.postMessageTier especial — cerca de 1 mensagem por segundo por canal, com limite de workspaceSlack — Rate limits, 2026-08-16
conversations.listTier 2 — 20+ requisições por minutoSlack — Rate limits, 2026-08-16
Timeout de entrega do dispatcher10 segundosSlackDispatcherService
Tentativas de entrega3, backoff 1 s → 2 s → 4 sSlackDispatcherService

Não há proteção de taxa própria no envio. Se o seu produto disparar rajada em um canal, o limite da Slack será atingido e o chat.postMessage falhará. Controle a cadência do seu lado (§15).

Catálogo de erros

StatusCódigoSignificaO que fazer
400VALIDATIONCorpo ou query reprovados no ZodConfira contra a §9
400VALIDATIONteamId and slackUserId query params are requiredFaltam os dois na listagem de canais
400VALIDATIONSlack error: <código>Erro do lado da Slack: channel_not_found, not_in_channel, invalid_auth. O código original vem junto
400VALIDATIONInvalid callback statestate adulterado, fluxo expirado, ou a chave mestra mudou entre connect e callback
400VALIDATIONSlack OAuth response missing required fieldsA resposta veio sem authed_user.access_token, sem authed_user.id ou sem team.id — quase sempre escopo de usuário não concedido
401—invalid signature em POST /eventsSLACK_SIGNING_SECRET divergente, timestamp fora dos 5 minutos, ou corpo alterado por proxy
403—Organization context requiredAutentique com token que carregue organizationId
403FORBIDDENFalta SLACK_READ ou SLACK_WRITEConfira o papel e as permissões contratadas
404NOT_FOUNDSlackConnection inexistente ou excluídaConfira teamId e slackUserId; a organização vem do token
500INTERNALFailed to decrypt Slack tokenA SLACK_CREDENTIAL_MASTER_KEY mudou. Rotação invalida todas as conexões
500INTERNALSlack postMessage failed / Slack conversations.list failedFalha de rede ou indisponibilidade da Slack
503—POST /dispatchersMODULE_WEBHOOKS_ENGINE_URL ausente em standalone

Observabilidade.

  • GET /slack/health responde vida e versão do build. Não testa banco, nem Slack, nem Webhooks Engine.
  • A ingestão loga em debug quando um evento é filtrado (Slack event noise-filtered — not forwarding) e quando não há dispatcher ativo. É o primeiro lugar a olhar quando "não chega nada".
  • Falha de ingestão vira warn com Slack event ingest error (non-fatal) — nunca erro HTTP, porque a resposta à Slack já saiu.
  • O histórico de entrega vive no Webhooks Engine, não aqui. Este building block não expõe endpoint de log de entrega (§15).

14

Segurança e compliance

Isolamento entre tenants. O organizationId vem do claim assinado do JWT, nunca do corpo. Cada router define requireOrganization e devolve 403 quando falta. Nos caminhos de envio e de listagem de canais, a organização compõe a chave de busca da conexão: getDecryptedToken(organizationId, teamId, slackUserId) bate na chave única (organizationId, teamId, slackUserId). Passar teamId de outro cliente não resolve conexão nenhuma — retorna 404.

Na ingestão o caminho é o inverso, e precisa ser: o evento vem da Slack sem token da plataforma, então a organização é derivada da conexão encontrada por (teamId, slackUserId), e é ela que vai no emitSlackPublic. A entrega herda o tenant do dono do workspace.

flowchart LR
  subgraph SAI["Saída — a organização vem do token"]
    T["JWT assinado<br/>claim organizationId"] --> K["Chave de busca<br/>org + teamId + slackUserId"]
    K --> CN["Conexão encontrada"]
    K -.->|"teamId de outro cliente<br/>não resolve nada"| NF["404"]
  end
  subgraph ENT["Entrada — a organização é derivada da conexão"]
    SLK["Evento da Slack<br/>sem token da plataforma"] --> BU["Busca por<br/>teamId + authorizations[0]"]
    BU --> CN2["Conexão encontrada"]
    CN2 --> ORG["organizationId da conexão<br/>vai no emitSlackPublic"]
    ORG --> DEL["A entrega herda o tenant<br/>do dono do workspace"]
  end

A rota pública se defende por assinatura. POST /events não tem autenticação da plataforma — a Slack não carrega o nosso token. A defesa é o HMAC-SHA256 sobre v0:{timestamp}:{corpo bruto} com o SLACK_SIGNING_SECRET, comparado em tempo constante, com tolerância de 5 minutos no timestamp contra reenvio de requisição capturada. A verificação vale para toda requisição, inclusive o desafio de verificação de URL: sem segredo configurado, a resposta é 401, e não um caminho aberto.

Como as credenciais OAuth são protegidas — evidência no código.

O quêComoOnde
Token de usuário do SlackAES-256-GCM, IV aleatório de 12 bytes por operação, authTag verificado na decifragem. Formato iv:authTag:ciphertext, tudo em hexsrc/slack/utils/crypto.ts
Envelope gravadoJSON {accessToken, refreshToken} cifrado antes de ir ao banco, nunca em claroslack-connection.service.ts
state do OAuth em trânsitoMesmo esquema. organizationId, userId e redirectUrl viajam cifrados dentro do stateconnect e handleCallback
Chave mestraSLACK_CREDENTIAL_MASTER_KEY, 64 hex (32 bytes), validada por regex a cada uso. Vive em SOPS, nunca no repositóriogetMasterKey()
Segredo do dispatcher32 bytes aleatórios, entregues uma vez na criação e repassados ao Webhooks Engine como cabeçalhoslack-dispatcher.service.ts

Nada de token volta pela API. GET /connections seleciona campo a campo — id, teamId, teamName, slackUserId, status — e a coluna accessToken não está na projeção. Não existe endpoint que devolva token. O secret do dispatcher aparece uma única vez, na resposta da criação.

Chave por módulo. SLACK_CREDENTIAL_MASTER_KEY é distinta de CALENDAR_CREDENTIAL_MASTER_KEY, EMAIL_CREDENTIAL_MASTER_KEY e das demais. Comprometer uma não entrega as outras. O custo é operacional: rotacionar invalida todas as conexões, porque não há versionamento de chave nem recifragem gradual. Rotação é evento planejado com reconexão de todos os usuários.

O escopo do token limita o alcance, e isso é a garantia mais forte. O fluxo pede user_scope, não bot token. Um token de usuário alcança apenas as DMs daquele usuário e os canais em que ele já está. Não existe caminho técnico para ler conversa privada de terceiros no workspace, mesmo com o token em mãos.

O caminho do conteúdo da mensagem, e onde ele para, é o que decide a conversa de retenção:

flowchart LR
  SL["Texto da mensagem<br/>no Slack"] --> BB["BB Slack<br/>atravessa o processo"]
  BB -.->|"NÃO persiste —<br/>não há tabela de mensagem"| X["schema slack"]
  BB --> BUS["Barramento de eventos"]
  BUS --> WE["Webhooks Engine<br/>log de entrega guarda o payload,<br/>e o payload carrega o texto"]
  WE --> CLI["Endpoint do cliente"]
  WE --> RET["Aqui é onde a política de<br/>retenção precisa ser definida"]

Que conteúdo de mensagem é armazenado. Nenhum. Não há tabela de mensagem no schema slack. O texto atravessa o processo — chega no evento, passa pelo filtro, é publicado no barramento e entregue — sem persistência neste building block. O que fica no banco é:

DadoOndeNatureza
teamId, teamName, slackUserIdslack_connectionsIdentificadores do workspace e do usuário
Token OAuthslack_connectionsCredencial, cifrada
scopes[]slack_connectionsMetadado da autorização
endpointUrl, customHeaders, watchedChannels[]slack_dispatchersConfiguração de entrega
userId, organizationIdAmbasIdentificadores internos

Onde o conteúdo pode ficar retido, então. No Webhooks Engine, cujo log de entrega guarda o payload — e o payload carrega o texto da mensagem. Se a sua política proíbe reter conteúdo de conversa fora do Slack, essa é a política de retenção a definir, e ela é do Webhooks Engine, não daqui.

LGPD. Mensagem é dado pessoal, e o conteúdo pode ser sensível — conversa de RH, discussão jurídica, dado de saúde num canal de benefícios. Cinco pontos:

  1. Base legal e finalidade. O consentimento OAuth do usuário no Slack não é a base legal do seu tratamento. Documente a finalidade e peça só os escopos que usa.
  2. O administrador do workspace é parte interessada. Em workspace corporativo com aprovação de app ligada, o administrador aprova a instalação — e essa aprovação é parte do registro de conformidade do seu cliente.
  3. Minimização por filtro. O filtro de ruído reduz o que sai do Slack. É controle de privacidade além de controle de custo: mensagem descartada não trafega, não é entregue e não é logada.
  4. Minimização por não armazenar. Nenhum conteúdo é persistido aqui. Preserve isso: não crie cache de mensagem sem revisar retenção.
  5. Eliminação. Não há endpoint de desconexão (§15). Remover uma conexão exige intervenção no banco, e isso não revoga o token no Slack — a autorização segue ativa até o usuário removê-la na conta dele. Atender pedido de eliminação exige os dois passos, manualmente.

Autenticação e permissões.

PermissãoConcede
SLACK_READListar conexões e canais
SLACK_WRITEConectar workspace, criar e atualizar dispatcher, enviar mensagem

Não há permissão de administrador separada: quem tem SLACK_WRITE cria dispatcher, e dispatcher define para onde o conteúdo do workspace é entregue. Trate SLACK_WRITE como permissão elevada ao montar os papéis.


15

Limitações conhecidas

Este building block está em alpha. A lista abaixo é longa de propósito.

LimitaçãoImpactoSituação
Não há desconectar workspaceNão existe DELETE /connections/:id. Remover uma conexão exige mexer no banco, e isso não revoga o token no SlackLacuna conhecida, registrada em `docs/slack/SETUP.md`. Relevante para LGPD (§14)
O token não é renovadoNão há refreshAccessToken. O manifesto recomendado desliga a rotação de token da Slack. Se a rotação for ligada no app, a conexão expira e só volta com nova autorizaçãoDepende de a rotação permanecer desligada
Não há listar, apagar ou pausar dispatcherSó POST e PATCH. Descobrir os dispatchers de uma conexão exige consulta ao banco; dispatcher errado não tem como ser removido pela APIRoadmap — os irmãos WPP têm tudo isso
Sem histórico de entrega pela APIO log vive no Webhooks Engine; este BB não expõe rota equivalente à GET /:id/delivery-logs do WPPRoadmap
Sem retentativa manual de entregaDepois das 3 tentativas automáticas, não há como reprocessar pela API do Slack BBRoadmap
slack.v1.message.sent nunca é emitidoO dispatcher registra a assinatura com esse filtro, mas nenhum código o publica. POST /messages/send não gera evento. Assinar esse tipo não entrega nadaReservado, não implementado — mesmo padrão do wpp.v1.message.status.updated
Sem resposta em threadslackSendSchema não aceita thread_ts. Toda resposta vai ao canal, fora do contexto da threadRoadmap. Bloqueante para alguns produtos
Sem Block Kit, anexo, arquivo ou reaçãoO envio é text puro. Sem botão, modal, slash command ou Socket ModeNão implementado. Se precisa disso, use a Slack Platform direta
Sem controle de taxa no envioNenhuma proteção contra o limite de aproximadamente 1 mensagem por segundo por canal do chat.postMessage. Rajada gera erro da SlackControle a cadência no seu lado
conversations.list sem paginaçãoA listagem de canais faz uma chamada e devolve o que veio. Workspace grande não vem completoNão implementado
A distribuição do app depende de revisão da SlackOs escopos de histórico (*:history) disparam revisão antes da distribuição fora do workspace de instalação. Enquanto ela não sai, o alcance é o workspace de desenvolvimentoBloqueio externo. O plano B — modelo de bot sem escopos de histórico — está descrito em `docs/slack/SETUP.md`
O app OAuth é global, não por tenantA tela de consentimento leva a marca do app da plataforma, não a do cliente. Diferente do CalendarPor desenho, seguindo o modelo de app distribuído da Slack
SLACK_CREDENTIAL_MASTER_KEY fora do schema de configuraçãoÉ lida direto de process.env e validada só no uso. Ausência ou formato errado não impedem o boot — quebram a primeira conexãoVerifique após o deploy (§13)
Sem facade de móduloNão há slack.facade.ts. Outros building blocks não conseguem chamar o Slack por HTTP em standaloneSó necessário quando outro BB precisar
/health não checa dependênciaVida e versão apenasSuficiente para liveness, insuficiente para readiness
Enums maiores que o comportamentoSlackConnectionStatus e SlackDispatcherStatus têm estados que nenhum caminho de código atribui — só ACTIVE é usadoSchema à frente da implementação
Prefixo de rota duplicadoA rota real é /slack/api/v1/slack/...Não corrigido

16

Perguntas frequentes

Isso está pronto para produção?

Não como produto completo. O caminho principal — conectar, ingerir com filtro, entregar por webhook, enviar mensagem — funciona e é coberto por testes unitários. O que falta é ciclo de vida: desconectar, listar e apagar dispatcher, renovar token, responder em thread. Para um piloto controlado, com um cliente e acompanhamento, dá para usar. Para vender como capacidade fechada, leia a §15 inteira antes.

Vocês guardam as mensagens do meu Slack?

Neste building block, não. Não existe tabela de mensagem. O texto atravessa o processo e é entregue ao seu endpoint sem ser persistido aqui. Mas o log de entrega do Webhooks Engine guarda o payload, e o payload tem o texto. Se conteúdo de conversa não pode ficar retido fora do Slack, a política de retenção a definir é a de lá.

O app consegue ler conversa privada de qualquer pessoa do workspace?

Não. O token é de usuário, e alcança as DMs daquele usuário e os canais em que ele já está. Não existe caminho técnico para ler DM de terceiros. Ainda assim, em workspace corporativo, a instalação costuma passar por aprovação do administrador — e deve mesmo.

Por que preciso de um app Slack meu se o building block já existe?

Você não precisa de um app seu: o Slack BB usa um app da plataforma, configurado por ambiente, que cada cliente instala no workspace dele. É diferente do Calendar, onde cada organização registra o próprio app OAuth. A contrapartida é que a tela de consentimento leva a marca da plataforma, e a aprovação do app junto à Slack é única e compartilhada (§15).

A mensagem aparece como bot ou como pessoa?

Como pessoa — a que instalou o app. O envio usa as_user: true com o token de usuário. Em canal operacional isso muda a taxa de leitura, e também significa que a mensagem é atribuída àquela pessoa no histórico do workspace. Deixe isso claro para quem conecta.

Recebo todas as mensagens do workspace?

Não, e é de propósito. O filtro entrega DM, grupo de DM, qualquer mensagem que mencione o usuário conectado, e canais comuns só se estiverem em watchedChannels. Mensagem de bot, editada, apagada e de entrada ou saída de canal é descartada antes de qualquer entrega. O diagrama da decisão está em §8.

Consigo responder dentro da thread?

Hoje não. O POST /messages/send não aceita thread_ts e a resposta vai para o canal (§15). Se thread é requisito, é bloqueante — planeje em torno disso ou aguarde.

Como sei se o dispatcher está entregando?

Pelo Webhooks Engine, que guarda o log com status, código HTTP e tentativas. Este building block não expõe rota de log de entrega (§15). Para saber se o problema é antes da entrega, os logs em debug do serviço dizem quando um evento foi filtrado ou quando não havia dispatcher ativo.

Qual a diferença entre este BB e o WPP?

O canal, e o grau de maturidade. O WPP fala WhatsApp por Baileys, o WPP Business fala WhatsApp pela Meta Cloud API, e este fala Slack. Os três entregam pelo mesmo Webhooks Engine, com a mesma semântica de assinatura e retry — por isso um consumidor serve para os três. O Slack é a versão mínima do padrão: um modo de dispatcher, sem condições JSONPath, sem listar ou apagar dispatcher, sem retentativa manual. A tabela de diferenças está em §12.

O que acontece se eu rotacionar a SLACK_CREDENTIAL_MASTER_KEY?

Todas as conexões param de decifrar, com Failed to decrypt Slack token, e todos os usuários precisam reconectar. Não há versionamento de chave nem recifragem gradual. Guarde a chave em SOPS e trate como segredo de longa duração.

Por que a rota tem slack duas vezes?

Porque o app usa basePath('/slack') e os routers são montados sob /api/v1/slack. A rota efetiva é /slack/api/v1/slack/events. Atenção especial nessa: é a Request URL que você cadastra no console do Slack, e errar o prefixo é a causa mais comum de a verificação de URL falhar.


Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md · Runbook: docs/slack/SETUP.md