Catalisa.
Building blocks/ComunicaçãoAlpha

Slack

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

8
Endpoints
2
Entidades
1
Provedores
Tenant
Escopo
3029
Porta

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

01Resumo 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.


02O problemanegó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.


03Proposta de valornegó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

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.


04Casos de uso reaisnegó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.

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.

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.

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.

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.


05Mercado e diferenciaisnegó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.

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


06Modelo de cobrança e ROInegó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. 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.


07Arquitetura

                    HTTP
                      │
  ┌───────────────────┴───────────────────────────────────────────────┐
  │ Hono app  basePath('/slack')                                       │
  │                                                                    │
  │  /api/v1/slack/connections   connectionRouter               (3)    │
  │  /api/v1/slack/dispatchers   dispatcherRouter               (2)    │
  │  /api/v1/slack/messages      messagesRouter                 (1)    │
  │  /api/v1/slack/channels      channelsRouter                 (1)    │
  │  /api/v1/slack/events        slackEventsRouter  ← Slack     (1)    │
  │  /health          /connected  (página de retorno pós-OAuth)        │
  └───────────────────┬───────────────────────────────────────────────┘
                      │
  ┌───────────────────┴───────────────────────────────────────────────┐
  │ services/                                                          │
  │   SlackConnectionService   OAuth v2, state cifrado, upsert          │
  │   SlackEventsService       assinatura, filtro de ruído, fan-out     │
  │   SlackSendService         chat.postMessage como o usuário          │
  │   SlackChannelsService     conversations.list                       │
  │   SlackDispatcherService   cria assinatura no Webhooks Engine       │
  └──────┬──────────────────────────────────────────┬─────────────────┘
         │                                          │
         ▼                                          ▼
  ┌──────────────────────┐              ┌────────────────────────────┐
  │ repositories/ Prisma │              │ @slack/web-api  WebClient  │
  │  schema "slack"      │              │  → api.slack.com           │
  │  2 modelos           │              └────────────────────────────┘
  └──────────────────────┘

Caminho de uma mensagem que entra

  Usuário escreve no Slack
          │
          ▼
  POST /slack/api/v1/slack/events        (rota pública — sem token da plataforma)
          │
          ├─ 1. lê o corpo BRUTO como texto, antes de qualquer parse
          ├─ 2. verifica x-slack-signature contra SLACK_SIGNING_SECRET
          │       HMAC-SHA256 de  v0:{timestamp}:{corpo}
          │       recusa se o timestamp desviar mais de 5 minutos  → 401
          ├─ 3. faz o parse do JSON                                → 400 se inválido
          ├─ 4. url_verification? devolve o challenge em texto puro
          │
          ├─ 5. event_callback de `message` ou `app_mention`:
          │        dispara ingestEvent SEM aguardar  ────────┐
          │                                                  │
          └─ 6. responde 200 imediatamente ◀─────────────────┼── janela de 3s da Slack
                                                             │
                                                             ▼
                                              SlackEventsService.ingestEvent
                                                             │
                                    ┌────────────────────────┴───────────────────┐
                                    │ 1. conexão por (team_id, authorizations[0]) │
                                    │ 2. dispatchers ACTIVE da conexão            │
                                    │ 3. união dos watchedChannels                │
                                    │ 4. shouldForward(...)                       │
                                    │      descarta bot_message, message_changed, │
                                    │      message_deleted, channel_join/leave     │
                                    │      passa: im, mpim, menção <@user>,        │
                                    │             canal em watchedChannels         │
                                    │ 5. emitSlackPublic(slack.v1.message.received)│
                                    └────────────────────────┬───────────────────┘
                                                             ▼
                                                    Webhooks Engine
                                              assinatura · retry · log
                                                             ▼
                                                   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.


08Conceitos 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

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

  evento chega
       │
       ▼
  subtype ∈ { bot_message, message_changed, message_deleted,
              channel_join, channel_leave } ?
       │ sim ──────────────────────────────────▶ DESCARTA
       │ não
       ▼
  channelType ∈ { im, mpim } ?
       │ sim ──────────────────────────────────▶ ENTREGA
       │ não
       ▼
  texto contém <@slackUserId da conexão> ?
       │ sim ──────────────────────────────────▶ ENTREGA  (menção)
       │ não
       ▼
  channelId ∈ união dos watchedChannels dos dispatchers ACTIVE ?
       │ sim ──────────────────────────────────▶ ENTREGA
       │ não ──────────────────────────────────▶ DESCARTA

Ciclo de vida da conexão

   POST /connections/connect
          │  gera state cifrado (organizationId + userId + redirectUrl)
          ▼
   usuário autoriza em slack.com/oauth/v2/authorize
          │
          ▼
   GET /connections/callback   (rota pública)
          │  decifra o state → oauth.v2.access → cifra o token → upsert
          ▼
     ┌──────────┐
     │  ACTIVE  │   reconectar faz upsert na mesma linha e limpa deletedAt
     └──────────┘

   Não há endpoint de desconexão — ver §15.
   Não há renovação de token — ver §15.

09Referê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

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

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

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

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

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

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

{
  "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": []
  }
}

10Iní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.

1. Autenticar no IAM

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

2. Confirmar que a rota de eventos está protegida

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

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

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

4. Verificar a conexão

curl -s "$BASE/connections" -H "Authorization: Bearer $TOKEN" | jq
{
  "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

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

6. Criar o dispatcher

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

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

7. Enviar uma mensagem

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
{ "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.


11Receitas

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.

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

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

Armadilhas.

  • O corpo substitui a lista inteira. Não há acrescentar nem remover — mande sempre o conjunto completo.
  • 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:

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


12Integraçã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.

   WhatsApp (Baileys)      WhatsApp (Meta Cloud)        Slack
          │                        │                      │
          ▼                        ▼                      ▼
   ┌────────────┐          ┌──────────────┐       ┌──────────────┐
   │  BB wpp    │          │ BB wpp-      │       │  BB slack    │
   │  wpp.v1.*  │          │ business     │       │ slack.v1.*   │
   └─────┬──────┘          └──────┬───────┘       └──────┬───────┘
         │                        │                      │
         └────────────┬───────────┴──────────────────────┘
                      ▼
          ┌───────────────────────────────┐
          │      Webhooks Engine          │
          │  assinatura RSA · retry ·     │
          │  log de entrega · filtros     │
          └───────────────┬───────────────┘
                          ▼
                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. 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).


13Configuraçã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 /dispatchers503 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
401invalid signature em POST /eventsSLACK_SIGNING_SECRET divergente, timestamp fora dos 5 minutos, ou corpo alterado por proxy
403Organization 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
503POST /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).

14Seguranç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.

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.

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.


15Limitaçõ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 dispatcherPOST 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

16Perguntas 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

Building blocks relacionados