SuperdindaDocs

Webhooks e eventos

A tabela events (ULID), como uma mudança de estado vira evento, os webhooks que chegam dos provedores pelo gateway e a situação dos webhooks de saída.

Dois fluxos parecidos, em sentidos opostos:

  • Pra dentro: provedores (WAHA, Meta) avisam o gateway por webhook. O gateway confere a assinatura e enfileira, e os workers aplicam.
  • Pra fora: cada mudança de estado do domínio vira uma linha em events e uma entrada no stream da conta, que o SSE entrega ao app.
sequenceDiagram
  autonumber
  participant P as WAHA / Meta
  participant G as gateway :3002
  participant PS as provider:nome<br/>(Redis Stream)
  participant W as workers
  participant DB as tabela events
  participant AS as events:account:id<br/>(Redis Stream)
  participant API as api
  participant App as app
  P->>G: webhook assinado
  G->>G: confere HMAC ou assinatura
  G->>PS: XADD
  G-->>P: 200 em milissegundos
  W->>PS: XREADGROUP (grupo workers)
  W->>W: aplica (pareamento, ingest)
  W->>DB: INSERT (a fonte da verdade)
  W->>AS: XADD (depois da linha)
  W->>PS: XACK
  App->>API: GET /accounts/{uid}/events?ticket=…
  API->>AS: XREAD a partir do Last-Event-ID
  API-->>App: SSE event: event
  Note over W,AS: Um handler que lança erro não confirma (XACK)<br/>e a entrada volta na próxima passada.

A tabela events

Append-only, criada na migração 0016_events (AD-010). É a fonte da verdade do que aconteceu, e o Redis é só o transporte.

ColunaO que é
idULID: ordena por tempo, é único entre processos
typeNome do evento, por exemplo pairing.paired
account_id, seat_idConta (sempre) e vaga (quando o evento é de uma pessoa)
sourceQuem gravou: api, gateway, workers ou crons
payloadJSON. Nunca leva conteúdo de conversa
occurred_atQuando foi gravado

Índice events_by_account (account_id, id). O cliente recebe um EventDto, com o seatUid público no lugar do id interno. A tabela abaixo sai direto do tipo em backend/src/domains/events/dtos/event.dto.ts:

Prop

Type

De mudança de estado a evento

backend/src/domains/events/use-cases/publish-event.ts tem três funções:

FunçãoO que fazQuando usar
publishEvent(input)Grava a linha e depois faz XADD em events:account:{id}O caso comum
recordEvent(input, tx)Só grava a linha, dentro da transação de quem chamaQuando o evento tem que existir exatamente se a mudança foi confirmada (por exemplo, alertas)
streamEvent(accountId, dto)Só faz o XADDDepois do commit, pra empurrar o que recordEvent gravou

A linha vem antes do stream de propósito: um stream que corre na frente da tabela poderia anunciar algo que nunca aconteceu. Os domínios embrulham isso em helpers próprios (publishConsentEvent, publishQueueEvent, publishPairingEvent). O de pareamento também escreve no stream da pessoa protegida.

Publicando um evento num use case
import { publishEvent } from "@domains/events/index.ts";

await publishEvent({
  type: "seat.paused",
  accountId: seat.account_id,
  seat, // vira seatUid no EventDto
  payload: { cause: "guardian", pausedAt, until },
  source: "api",
});

O catálogo dos tipos fica em Tempo real.

Webhooks de entrada (gateway)

O processo gateway (backend/apps/gateway/main.ts) recebe os webhooks, confere, enfileira e responde em milissegundos. Ele nunca toca nas tabelas do domínio: banco lento não pode fazer o provedor desistir da gente.

RotaProvedorVerificaçãoVai pra
POST /webhooks/wahaWAHAHMAC-SHA512 do corpo no header X-Webhook-Hmac, com WAHA_WEBHOOK_HMAC_KEY. Sem a chave o gateway nem sobeprovider:waha
GET /webhooks/whatsappMeta (handshake)hub.verify_token igual a WABA_VERIFY_TOKENResponde hub.challenge
POST /webhooks/whatsappMeta (recibos de entrega e respostas)X-Hub-Signature-256 com WABA_APP_SECRETprovider:meta, uma entrada por evento do corpo
GET /health——Estado do Redis

Assinatura inválida responde 401, e corpo que não é evento do WAHA responde 400. Essas respostas não usam o envelope da API.

Nos workers, consumeProviderEvents lê cada stream como consumer group workers. Um handler que termina confirma a entrada (XACK), e um que lança erro deixa a entrada pendente pra próxima passada. No WAHA, eventos de sessão mudam o estado do pareamento (applyProviderState), e message/message.any entram no ingest. Evento de sessão desconhecida é descartado. Os detalhes de WAHA, GOWS e pareamento ficam em Instances.

Webhooks de saída

Não existem. O backend não manda webhooks pra clientes nem pra terceiros. Quem precisa reagir a um evento é o próprio app, pelo SSE. As notificações pros responsáveis (caixa de entrada, e-mail, push) são outro mecanismo, descrito em Alertas.

Em aberto

  • Não há política de expiração de eventos antigos na tabela events (pendência registrada na AD-010).
  • O webhook de cobrança POST /webhooks/asaas (G-33 do api-gaps.md) ainda não existe.
  • Webhook de saída pra parceiros nunca foi discutido em AD ou spec. Se aparecer, precisa de decisão própria.

Fontes

  • backend/src/domains/events/** (publish-event.ts, tables/events.table.ts, dtos/event.dto.ts, entities/event.entity.ts)
  • backend/migrations/0016_events.ts
  • backend/apps/gateway/main.ts, backend/src/infra/provider-stream.ts, backend/src/infra/streams.ts
  • backend/apps/workers/consume-provider-events.ts, backend/apps/workers/waha-events.ts
  • backend/src/domains/channels/webhooks/waha-webhook.ts, meta-webhook.ts
  • backend/src/domains/channels/use-cases/publish-pairing-event.ts
  • .specs/features/redesign/blueprint.md, api-gaps.md (G-33)
  • AD-010

On this page