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.
- 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
- 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
- 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.
| Atributo | Valor |
|---|---|
| Identificador | slack |
| Categoria | Comunicação |
| Escopo | Tenant (exige organizationId no token) |
| Porta (standalone) | 3029 |
| Path alias | @slack |
| Prefixo HTTP | /slack/api/v1/slack — sim, slack aparece duas vezes |
| Status | Alpha desde 2026-06 |
| Depende de | PostgreSQL (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
| Antes | Depois |
|---|---|
| Construir e manter um app Slack distribuído por conta própria | Um app configurado uma vez, com o OAuth por workspace já implementado |
| Token de instalação guardado como der | AES-256-GCM no banco, envelope cifrado, nunca devolvido pela API |
| Handler que processa dentro dos 3 segundos e reenvia duplicado | Ack imediato e processamento disparado fora do caminho da resposta |
| Todo evento do workspace chegando ao seu backend | DM, 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ério | Catalisa Slack | Slack API direta | Zapier | n8n | Knock |
|---|---|---|---|---|---|
| Ingestão de mensagem do usuário | Sim, com filtro de ruído | Você constrói | Sim, por trigger | Sim | Não é o foco |
| Envio de mensagem | Sim, como o usuário conectado | Sim | Sim | Sim | Sim, como bot |
| Multi-tenant por workspace | Sim, ligado à organização do IAM | Você constrói | Por conta conectada | Você constrói | Por tenant do produto |
| Modelo de preço | Em definição | Sem licença de API | Por 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ça | Free 10 mil msg; Starter US$ 250/mês |
| Onde o fluxo vive | No seu código | No seu código | Na conta Zapier | No n8n | No Knock |
| Filtro de ruído embutido | Sim | Você constrói | Você configura | Você configura | Não se aplica |
| Retry e assinatura na entrega | Via Webhooks Engine | Você constrói | Interno | Interno | Interno |
| Preferências por usuário e agrupamento | Não | Não | Não | Não | Sim, é o forte deles |
| Block Kit, modais, Socket Mode | Não (§15) | Sim | Parcial | Parcial | Parcial |
| Maturidade | Alpha (§15) | Produção | Produção | Produção | Produçã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
- 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. - 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.
- 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. - 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.
| Driver | Por que pesa |
|---|---|
| Workspaces conectados | Cada conexão é um token cifrado para guardar e um conjunto de dispatchers para manter |
| Mensagens ingeridas | Cada evento aceito pelo filtro vira uma entrega pelo Webhooks Engine, com retry se falhar |
| Mensagens enviadas | Cada 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 Slack | Zapier | n8n Cloud | Knock | |
|---|---|---|---|---|
| Base de cálculo | Em definição | Por tarefa | Por execução de workflow | Por mensagem |
| Ordem de grandeza mensal | A definir | 40 mil tarefas/mês fica bem acima da faixa de entrada de US$ 19,99 | 40 mil execuções exigem o plano Business, na casa de 667 €/mês | 40 mil mensagens cabem no Starter de US$ 250/mês |
| Ingestão bidirecional | Sim | Sim | Sim | Não é o foco |
| Filtro antes de contar | Sim | Não — conta o que entra | Não | Não se aplica |
| App Slack distribuído | Você ainda precisa aprovar o seu (§15) | Zapier usa o app deles | Você constrói | Knock 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: semSLACK_SIGNING_SECRETconfigurado, ou com assinatura inválida, a resposta é401mesmo 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 otimingSafeEqualdo 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 exige2xxem 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 dewarn, 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_SECRETeSLACK_CALLBACK_URLvê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 escopochat:writede usuário, e a mensagem conta como escrita por aquela pessoa. - O filtro considera a união dos
watchedChannelsde todos os dispatchers ativos. Se um dispatcher observa#ae outro observa#b, a mensagem de#apassa 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
| Termo | Significa |
|---|---|
| Connection | O 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). |
| Team | O workspace do Slack. O teamId (T…) é como a Slack o identifica em todo evento. |
| Slack user id | O identificador do usuário no workspace (U…). Quem instalou o app e em nome de quem as mensagens saem. |
| Dispatcher | A regra de encaminhamento: para qual endpoint entregar e quais canais observar. Cria uma assinatura no Webhooks Engine. |
| Watched channels | Lista de channelId observados. Só afeta canais comuns — DM e menção passam sempre. |
| Signing secret | Segredo do app Slack usado para verificar a assinatura de cada requisição recebida. Global, do ambiente. |
| Envelope cifrado | JSON {accessToken, refreshToken} cifrado com AES-256-GCM e gravado como uma string. |
| Evento público | slack.v1.message.received, publicado no barramento e entregue pelo Webhooks Engine. |
Modelo de dados — schema slack no PostgreSQL.
| Modelo Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
SlackConnection | slack.slack_connections | Workspace conectado por um usuário | teamId, teamName, slackUserId, accessToken (envelope cifrado), scopes[], status, único (organizationId, teamId, slackUserId) |
SlackDispatcher | slack.slack_dispatchers | Regra de encaminhamento | endpointUrl, customHeaders, watchedChannels[], subscriptionId, status |
Enumerações
| Enum | Valores |
|---|---|
SlackConnectionStatus | ACTIVE e demais estados do schema. Só ACTIVE é atribuído hoje |
SlackDispatcherStatus | ACTIVE e demais estados do schema. Só ACTIVE é atribuído hoje |
Escopos de usuário solicitados
| Escopo | Para quê |
|---|---|
im:history · im:read | Ler e listar DMs |
mpim:history · mpim:read | Ler e listar grupos de DM |
channels:history · channels:read | Ler e listar canais públicos dos quais o usuário participa |
groups:history · groups:read | Ler e listar canais privados dos quais o usuário participa |
users:read | Resolver nome e perfil |
chat:write | Postar 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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /slack/api/v1/slack/connections/connect | Devolve a URL de autorização do Slack | SLACK_WRITE |
GET | /slack/api/v1/slack/connections/callback | Callback OAuth. Rota pública — sem authMiddleware | Pública |
GET | /slack/api/v1/slack/connections | Lista as conexões do usuário do token | SLACK_READ |
Dispatchers — /slack/api/v1/slack/dispatchers
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /slack/api/v1/slack/dispatchers | Cria dispatcher e a assinatura no Webhooks Engine. 201 | SLACK_WRITE |
PATCH | /slack/api/v1/slack/dispatchers/:id | Atualiza apenas watchedChannels | SLACK_WRITE |
Mensagens e canais
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /slack/api/v1/slack/messages/send | Envia mensagem como o usuário conectado | SLACK_WRITE |
GET | /slack/api/v1/slack/channels?teamId=&slackUserId= | Lista canais, DMs e grupos da conexão | SLACK_READ |
Eventos do Slack — rota pública
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /slack/api/v1/slack/events | Recebe url_verification e event_callback da Slack | Pública, autenticada por x-slack-signature |
Saúde e retorno de OAuth
| Método | Rota | Descrição |
|---|---|---|
GET | /slack/health | Sonda de vida e versão do build |
GET | /slack/connected | Pá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" }
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
redirectUrl | string (URL) | Sim | Para 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
| Status | Quando |
|---|---|
400 | redirectUrl ausente ou não é URL válida |
403 | Sem SLACK_WRITE ou sem organização no token |
500 | SLACK_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" }
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
connectionId | string (UUID) | Sim | Conexão à qual o dispatcher pertence |
teamId | string | Sim | Workspace. Compõe o nome da assinatura |
slackUserId | string | Sim | Usuário conectado. Compõe o nome da assinatura |
endpointUrl | string (URL) | Sim | Para onde o Webhooks Engine entrega |
watchedChannels | string[] | Não | Canais comuns observados. Padrão [] — só DM e menção passam |
customHeaders | object | Não | Cabeçalhos extras em cada entrega |
Resposta 201
{
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"secret": "c0ffee…",
"subscriptionId": "aaaa-bbbb-cccc-dddd"
}
}
O
secretde 32 bytes é gerado aqui e enviado ao Webhooks Engine como o cabeçalhox-webhook-secretde 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
| Status | Quando |
|---|---|
400 | Corpo reprovado no Zod |
403 | Sem SLACK_WRITE ou sem organização |
503 | Em 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?"
}
| Campo | Tipo | Obrigatório | Restrições |
|---|---|---|---|
teamId | string | Sim | Mínimo 1 caractere |
slackUserId | string | Sim | Mínimo 1 caractere |
channelId | string | Sim | ID do canal, DM ou grupo (C…, D…, G…) |
text | string | Sim | 1 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
| Status | Quando |
|---|---|
400 | Corpo reprovado no Zod, ou a Slack devolveu erro (Slack error: channel_not_found, not_in_channel, ...) |
403 | Sem SLACK_WRITE ou sem organização |
404 | Não há conexão para essa tripla, ou ela foi excluída |
500 | Falha 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çalho | Uso |
|---|---|
x-slack-request-timestamp | Época em segundos. Desvio maior que 5 minutos é recusado |
x-slack-signature | v0= seguido do HMAC-SHA256 de v0:{timestamp}:{corpo bruto} com o SLACK_SIGNING_SECRET |
Respostas
| Situação | Resposta |
|---|---|
Assinatura inválida, ausente, fora da janela, ou SLACK_SIGNING_SECRET não configurado | 401 {"error":"invalid signature"} |
| Corpo não é JSON | 400 {"error":"invalid json"} |
type: url_verification | 200 com o challenge em texto puro |
type: url_verification sem challenge string | 400 {"error":"missing challenge"} |
event_callback de message ou app_mention | 200 {"ok":true} — ingestão dispara em paralelo |
| Qualquer outro tipo | 200 {"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
tspode chegar duas vezes. Usets+channelIdcomo 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.
- Só
watchedChannelsé atualizável. Para mudarendpointUrloucustomHeaders, crie outro dispatcher; não háDELETEpara 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
correlationIdque volta é otsda 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ção | Como | O que significa falhar |
|---|---|---|
| A Request URL do app aponta para a rota certa? | Console do Slack, Event Subscriptions | Deve ser {BASE_PUBLICA}/slack/api/v1/slack/events, com o slack duplicado |
| A Slack consegue verificar a URL? | O console mostra Verified | 401 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ário | Faltam message.im, message.mpim, message.channels, message.groups, app_mention |
| A conexão existe? | GET /connections | Se a lista está vazia, o OAuth não concluiu |
| Há dispatcher ativo? | Não há endpoint de listagem (§15) — consulte slack.slack_dispatchers | Sem dispatcher ACTIVE, ingestEvent sai antes de publicar |
| O canal está observado? | PATCH /dispatchers/:id | Canal 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ço | Em standalone, sem MODULE_WEBHOOKS_ENGINE_URL a criação do dispatcher já teria dado 503 |
Atalho: teste sempre com DM primeiro. DM ignora
watchedChannelse elimina metade das causas possíveis em uma tentativa.
12Integração com outros building blocks
| Building block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o token com organizationId e as permissões SLACK_* | Sim |
| Webhooks Engine | Entrega os eventos com assinatura, retry e log. O dispatcher cria a assinatura | Sim, para dispatcher |
| WPP | Mesmo padrão de dispatcher. Quem consome WhatsApp consome Slack com o mesmo verificador | Não |
| WPP Business | Idem, pelo canal Meta Cloud API | Não |
| Calendar | Padrão irmão de OAuth por conexão, com credenciais cifradas — com uma diferença importante (abaixo) | Não |
| Audit Trail | Registro de quem conectou workspace e criou dispatcher | Nã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-business | slack | |
|---|---|---|
| Modos de dispatcher | BASIC e ADVANCED, com condições JSONPath | Um modo só |
| Filtro por padrão glob de evento | Sim | Não — um tipo de evento só |
| Listar, apagar e pausar dispatcher | Sim | Não (§15) |
| Histórico de entregas por API | Sim | Não (§15) |
| Retentativa manual de entrega | Sim | Não (§15) |
| Namespace | wpp.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ável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
SLACK_CLIENT_ID | Client ID do app Slack | Sim | — |
SLACK_CLIENT_SECRET | Client Secret do app Slack | Sim | — |
SLACK_SIGNING_SECRET | Signing Secret. Sem ele, POST /events recusa tudo com 401 | Sim | — |
SLACK_CALLBACK_URL | {BASE_PUBLICA}/slack/api/v1/slack/connections/callback. Idêntico ao cadastrado no app | Sim | — |
SLACK_CREDENTIAL_MASTER_KEY | Chave AES-256-GCM. 64 caracteres hex (32 bytes). openssl rand -hex 32 | Sim | — |
MODULE_WEBHOOKS_ENGINE_URL | URL interna do Webhooks Engine. Sem ela, POST /dispatchers dá 503 em standalone | Em standalone | '' |
MODULE_SLACK_URL | URL interna do serviço | Em standalone | '' |
MODULE_SLACK_PORT | Porta em standalone | Não | 3029 |
DATABASE_URL | PostgreSQL, schema slack | Sim | — |
JWT_SECRET | Compartilhado com o IAM, mínimo 44 caracteres | Sim | — |
Cuidado na primeira subida.
SLACK_CLIENT_ID,SLACK_CLIENT_SECRETeSLACK_CALLBACK_URLsã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. ASLACK_CREDENTIAL_MASTER_KEYé lida direto deprocess.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 chamandoPOST /connections/connect: se ela devolve umaauthorizationUrlcom oclient_idcerto, está tudo no lugar.
Dependências de infraestrutura
| Dependência | Para quê |
|---|---|
| PostgreSQL | Schema slack, 2 tabelas |
| IAM | Emissão e validação do token |
| Webhooks Engine | Assinatura, retry e log de entrega dos dispatchers |
| Slack Platform | OAuth, chat.postMessage, conversations.list, Events API |
Limites e quotas
| Limite | Valor | Origem |
|---|---|---|
| Tamanho do texto enviado | 1 a 40.000 caracteres | Schema Zod |
| Janela de ack do Events API | 3 segundos, com 3 reenvios (imediato, 1 min, 5 min) | Slack — Events API, 2026-08-16 |
| Tolerância do timestamp da assinatura | 5 minutos | verifySlackSignature |
chat.postMessage | Tier especial — cerca de 1 mensagem por segundo por canal, com limite de workspace | Slack — Rate limits, 2026-08-16 |
conversations.list | Tier 2 — 20+ requisições por minuto | Slack — Rate limits, 2026-08-16 |
| Timeout de entrega do dispatcher | 10 segundos | SlackDispatcherService |
| Tentativas de entrega | 3, backoff 1 s → 2 s → 4 s | SlackDispatcherService |
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.postMessagefalhará. Controle a cadência do seu lado (§15).
Catálogo de erros
| Status | Código | Significa | O que fazer |
|---|---|---|---|
400 | VALIDATION | Corpo ou query reprovados no Zod | Confira contra a §9 |
400 | VALIDATION | teamId and slackUserId query params are required | Faltam os dois na listagem de canais |
400 | VALIDATION | Slack error: <código> | Erro do lado da Slack: channel_not_found, not_in_channel, invalid_auth. O código original vem junto |
400 | VALIDATION | Invalid callback state | state adulterado, fluxo expirado, ou a chave mestra mudou entre connect e callback |
400 | VALIDATION | Slack OAuth response missing required fields | A 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 /events | SLACK_SIGNING_SECRET divergente, timestamp fora dos 5 minutos, ou corpo alterado por proxy |
403 | — | Organization context required | Autentique com token que carregue organizationId |
403 | FORBIDDEN | Falta SLACK_READ ou SLACK_WRITE | Confira o papel e as permissões contratadas |
404 | NOT_FOUND | SlackConnection inexistente ou excluída | Confira teamId e slackUserId; a organização vem do token |
500 | INTERNAL | Failed to decrypt Slack token | A SLACK_CREDENTIAL_MASTER_KEY mudou. Rotação invalida todas as conexões |
500 | INTERNAL | Slack postMessage failed / Slack conversations.list failed | Falha de rede ou indisponibilidade da Slack |
503 | — | POST /dispatchers | MODULE_WEBHOOKS_ENGINE_URL ausente em standalone |
Observabilidade.
GET /slack/healthresponde vida e versão do build. Não testa banco, nem Slack, nem Webhooks Engine.- A ingestão loga em
debugquando 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
warncomSlack 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ê | Como | Onde |
|---|---|---|
| Token de usuário do Slack | AES-256-GCM, IV aleatório de 12 bytes por operação, authTag verificado na decifragem. Formato iv:authTag:ciphertext, tudo em hex | src/slack/utils/crypto.ts |
| Envelope gravado | JSON {accessToken, refreshToken} cifrado antes de ir ao banco, nunca em claro | slack-connection.service.ts |
state do OAuth em trânsito | Mesmo esquema. organizationId, userId e redirectUrl viajam cifrados dentro do state | connect e handleCallback |
| Chave mestra | SLACK_CREDENTIAL_MASTER_KEY, 64 hex (32 bytes), validada por regex a cada uso. Vive em SOPS, nunca no repositório | getMasterKey() |
| Segredo do dispatcher | 32 bytes aleatórios, entregues uma vez na criação e repassados ao Webhooks Engine como cabeçalho | slack-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 é:
| Dado | Onde | Natureza |
|---|---|---|
teamId, teamName, slackUserId | slack_connections | Identificadores do workspace e do usuário |
| Token OAuth | slack_connections | Credencial, cifrada |
scopes[] | slack_connections | Metadado da autorização |
endpointUrl, customHeaders, watchedChannels[] | slack_dispatchers | Configuração de entrega |
userId, organizationId | Ambas | Identificadores 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:
- 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.
- 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.
- 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.
- Minimização por não armazenar. Nenhum conteúdo é persistido aqui. Preserve isso: não crie cache de mensagem sem revisar retenção.
- 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ão | Concede |
|---|---|
SLACK_READ | Listar conexões e canais |
SLACK_WRITE | Conectar 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ção | Impacto | Situação |
|---|---|---|
| Não há desconectar workspace | Não existe DELETE /connections/:id. Remover uma conexão exige mexer no banco, e isso não revoga o token no Slack | Lacuna conhecida, registrada em `docs/slack/SETUP.md`. Relevante para LGPD (§14) |
| O token não é renovado | Nã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ção | Depende de a rotação permanecer desligada |
| Não há listar, apagar ou pausar dispatcher | Só POST e PATCH. Descobrir os dispatchers de uma conexão exige consulta ao banco; dispatcher errado não tem como ser removido pela API | Roadmap — os irmãos WPP têm tudo isso |
| Sem histórico de entrega pela API | O log vive no Webhooks Engine; este BB não expõe rota equivalente à GET /:id/delivery-logs do WPP | Roadmap |
| Sem retentativa manual de entrega | Depois das 3 tentativas automáticas, não há como reprocessar pela API do Slack BB | Roadmap |
slack.v1.message.sent nunca é emitido | O 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 nada | Reservado, não implementado — mesmo padrão do wpp.v1.message.status.updated |
| Sem resposta em thread | slackSendSchema não aceita thread_ts. Toda resposta vai ao canal, fora do contexto da thread | Roadmap. Bloqueante para alguns produtos |
| Sem Block Kit, anexo, arquivo ou reação | O envio é text puro. Sem botão, modal, slash command ou Socket Mode | Não implementado. Se precisa disso, use a Slack Platform direta |
| Sem controle de taxa no envio | Nenhuma proteção contra o limite de aproximadamente 1 mensagem por segundo por canal do chat.postMessage. Rajada gera erro da Slack | Controle a cadência no seu lado |
conversations.list sem paginação | A listagem de canais faz uma chamada e devolve o que veio. Workspace grande não vem completo | Não implementado |
| A distribuição do app depende de revisão da Slack | Os 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 desenvolvimento | Bloqueio 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 tenant | A tela de consentimento leva a marca do app da plataforma, não a do cliente. Diferente do Calendar | Por 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ão | Verifique após o deploy (§13) |
| Sem facade de módulo | Não há slack.facade.ts. Outros building blocks não conseguem chamar o Slack por HTTP em standalone | Só necessário quando outro BB precisar |
/health não checa dependência | Vida e versão apenas | Suficiente para liveness, insuficiente para readiness |
| Enums maiores que o comportamento | SlackConnectionStatus e SlackDispatcherStatus têm estados que nenhum caminho de código atribui — só ACTIVE é usado | Schema à frente da implementação |
| Prefixo de rota duplicado | A 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