matheus@devops:~$ cat sobre.txtReceptor de webhook grau-de-produção pra UAZAPI (gateway HTTP de WhatsApp). Persiste toda mensagem inbound antes de fazer qualquer coisa, agrupa burst do mesmo sender com janela de debounce configurável, e dispatcha os grupos resultantes pra rotas configuráveis — com retry de backoff exponencial, dead-letter queue, e uma API admin.
- Fastify 5 + Node 20, ~1.5k LoC, três deps (
fastify,pg,pino) - Zero loss by design — todo webhook cai em
raw_messages(PG) antes do handler retornar 200 pra UAZAPI; duplicata colapsa emUNIQUE(uazapi_message_id) - Debounce buffer — janela deslizante por
(sender, chat)dá flush depois de N segundos de silêncio (default 8s), previne tempestade de mensagens downstream - Routes são dado, não código — destinos vivem em
webhook_routese podem ser editados em runtime via API admin - Manifests K8s inclusos — namespace, deployment hardened (non-root,
seccompProfile: RuntimeDefault,drop: [ALL]), NetworkPolicies (default-deny + 5 allows explícitos), probes de readiness/liveness - Transcrição de áudio opcional — opt-in por rota, baixa da UAZAPI, descriptografa mídia WhatsApp (HKDF + AES-256-CBC), transcreve via Groq Whisper
matheus@devops:~$ ls stack/matheus@devops:~$ cat arquitetura.txtUAZAPI (gateway WhatsApp)
│
│ POST /webhook/{secret}
▼
┌──────────────────────┐
│ uazapi-webhook │ 1) Insere em raw_messages (PG)
│ Fastify + Node 20 │ UNIQUE(uazapi_message_id) → idempotente
│ (porta 4600) │ 2) Retorna 200 OK imediatamente
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ Debounce engine │ Agrupa por (sender, chat); flush depois
│ Map em memória │ de 8s de silêncio (DEBOUNCE_MS)
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ message_groups (PG) │
└──────────┬───────────┘
│ SELECT … FROM webhook_routes
│ WHERE chat_jid_filter / instance_id_filter match
│ ORDER BY priority DESC LIMIT 1
▼
┌──────────────────────┐
│ Router / Dispatcher │ destination_type ∈ { log, http }
│ (com retry) │ transcribe_audio? → chama transcriber
└──────────┬───────────┘
│
┌───┴────────────────────────┐
▼ ▼
delivered retry × N → dead_letters
matheus@devops:~$ ls endpoints/| Método | Path | Auth | Pra quê |
|---|---|---|---|
POST |
/webhook/:secret |
secret na URL | Webhook inbound da UAZAPI |
GET |
/health |
nenhuma | Liveness/readiness |
GET |
/api/stats |
X-API-Key |
Counts de raw_messages / message_groups / dead_letters |
GET |
/api/queue |
X-API-Key |
Buffer de debounce em memória |
GET |
/api/dead-letters |
X-API-Key |
Dispatches falhados (paginado) |
POST |
/api/retry/:id |
X-API-Key |
Re-dispatch de um grupo falhado |
GET / POST / PUT / DELETE |
/api/routes |
X-API-Key |
CRUD de rotas |
GET / POST |
/api/groups |
X-API-Key |
Helpers de client_groups |
POST |
/api/groups/:id/connect |
X-API-Key |
Registra essa URL de webhook na UAZAPI pra instância do grupo |
POST |
/api/groups/connect-all |
X-API-Key |
Mesma coisa, pra todo grupo habilitado |
matheus@devops:~$ ./quick-start.shgit clone https://github.com/MatheusHenriquePrates/uazapi-webhook.git
cd uazapi-webhook
# 1. Configura
cp .env.example .env
# edita .env: no mínimo WEBHOOK_SECRET, ADMIN_API_KEY, DATABASE_URL
# 2. Schema
psql "\$DATABASE_URL" -f sql/init.sql
psql "\$DATABASE_URL" -f sql/002_group_routing.sql
# 3. Roda
docker build -t uazapi-webhook:latest .
docker run --rm --env-file .env -p 4600:4600 uazapi-webhook:latest
# 4. Sanity check
curl -s http://localhost:4600/health
# {"status":"ok","uptime":3,"timestamp":"..."}matheus@devops:~$ cat routes.txtUma rota é uma linha em webhook_routes descrevendo pra onde (e como) encaminhar grupos de mensagem debouncados. O router pega a rota habilitada de maior prioridade cujos chat_jid_filter e instance_id_filter batem (NULL = wildcard).
Forward de tudo pra sua API:
curl -X POST http://localhost:4600/api/routes \
-H "X-API-Key: \$ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "forward-all",
"destination_type": "http",
"destination_config": {
"url": "https://my-app.example.com/incoming-whatsapp",
"headers": { "Authorization": "Bearer my-app-token" },
"timeout_ms": 30000
},
"enabled": true,
"priority": 10
}'Pra transcribe_audio: true de fato funcionar você precisa setar UAZAPI_BASE_URL, UAZAPI_INSTANCE_TOKENS_JSON, e GROQ_API_KEY.
Payload dispatchado pra destination_type = http:
{
"groupId": "uuid",
"senderJid": "5511...@s.whatsapp.net",
"chatJid": "120000000000000000@g.us",
"instanceId": "default",
"messageCount": 3,
"message": "[WhatsApp] From: 5511...\nChatJid: ...\nMessages: 3\n---\n...",
"transcription": null,
"messages": [{ "id": "...", "type": "text", "text": "...", "pushName": "...", "timestamp": "...", "payload": { } }]
}matheus@devops:~$ cat schema.txtQuatro tabelas; veja sql/init.sql e sql/002_group_routing.sql:
| Tabela | Lifecycle |
|---|---|
raw_messages |
queued → grouped (depois do flush do debounce) |
message_groups |
pending → delivered | failed |
webhook_routes |
Config estática; input principal do router |
client_groups |
Helper opcional pros endpoints /api/groups |
dead_letters |
Uma linha por message_group que esgotou retries |
matheus@devops:~$ cat k8s-deploy.txt# Build e import da imagem no k3s
docker build -t uazapi-webhook:latest .
docker save uazapi-webhook:latest | sudo k3s ctr images import -
# Edita o secret com valores reais, aplica
cp k8s/secret.example.yaml k8s/secret.yaml
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/secret.yaml
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
kubectl apply -f k8s/networkpolicy.yaml
kubectl get pods -n uazapi-webhook
kubectl logs -n uazapi-webhook deploy/uazapi-webhook -fO deployment vem hardened por default: runAsNonRoot, seccompProfile: RuntimeDefault, drop: [ALL], e seis NetworkPolicy (default-deny-all + allows explícitos pra DNS, HTTPS egress, Postgres, porta inbound do webhook, e relay opcional).
matheus@devops:~$ cat limitacoes.txt- O buffer de debounce é in-memory. No crash,
raw_messagesainda emqueuedsão recuperados no startup e re-alimentados no buffer — mas um flush em voo pode se perder. O dead-letter é a rede de segurança. - O dispatcher só implementa
logehttp. Adicionadestination_typepróprio estendendoattemptDispatchemsrc/router.js. - Sem streaming, sem rate-limit inbound. Põe reverse proxy na frente se expõe o webhook na Internet pública.
- A comparação estática estilo
BEARER_TOKENda API key não é constant-time. Trata o host como appliance privado.
matheus@devops:~$ cat LICENSEMIT — veja LICENSE.
matheus@devops:~$ contactmatheus@devops:~$ _matheus@devops:~$ cat about.txtProduction-grade webhook receiver for UAZAPI (WhatsApp HTTP gateway). Persists every inbound message before doing anything else, groups bursts from the same sender with a configurable debounce window, and dispatches the resulting message groups to configurable routes — with exponential-backoff retry, dead-letter queue, and an admin API.
- Fastify 5 + Node 20, ~1.5k LoC, three deps (
fastify,pg,pino) - Zero loss by design — every webhook lands in
raw_messages(PG) before the handler returns 200 to UAZAPI; duplicates collapse onUNIQUE(uazapi_message_id) - Debounce buffer — sliding window per
(sender, chat)flushes after N seconds of silence (default 8s) - Routes are data, not code — destinations live in
webhook_routesand can be edited at runtime through the admin API - K8s manifests included — namespace, hardened deployment, NetworkPolicies (default-deny + 5 explicit allows), readiness/liveness probes
- Optional audio transcription — opt-in per route, downloads from UAZAPI, decrypts WhatsApp media (HKDF + AES-256-CBC), transcribes via Groq Whisper
matheus@devops:~$ ls stack/matheus@devops:~$ cat endpoints.txt| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/webhook/:secret |
URL secret | Inbound UAZAPI webhook |
GET |
/health |
none | Liveness/readiness |
GET |
/api/stats |
X-API-Key |
Counts of raw_messages / message_groups / dead_letters |
GET |
/api/queue |
X-API-Key |
Current in-memory debounce buffer |
GET |
/api/dead-letters |
X-API-Key |
Failed dispatches (paginated) |
POST |
/api/retry/:id |
X-API-Key |
Re-dispatch a failed message group |
GET / POST / PUT / DELETE |
/api/routes |
X-API-Key |
CRUD on routes |
GET / POST |
/api/groups |
X-API-Key |
Helpers for client_groups |
matheus@devops:~$ ./quick-start.shgit clone https://github.com/MatheusHenriquePrates/uazapi-webhook.git
cd uazapi-webhook
cp .env.example .env
# edit .env: at minimum WEBHOOK_SECRET, ADMIN_API_KEY, DATABASE_URL
psql "\$DATABASE_URL" -f sql/init.sql
psql "\$DATABASE_URL" -f sql/002_group_routing.sql
docker build -t uazapi-webhook:latest .
docker run --rm --env-file .env -p 4600:4600 uazapi-webhook:latest
curl -s http://localhost:4600/healthmatheus@devops:~$ cat schema.txt| Table | Lifecycle |
|---|---|
raw_messages |
queued → grouped (after debounce flush) |
message_groups |
pending → delivered | failed |
webhook_routes |
Static config; primary input to the router |
client_groups |
Optional helper for the /api/groups endpoints |
dead_letters |
One row per message_group that exhausted retries |
matheus@devops:~$ cat limitations.txt- The debounce buffer is in-memory. On crash,
raw_messagesrows still inqueuedare recovered at startup and re-fed into the buffer — but a flush in flight can be lost. - The dispatcher only implements
logandhttp. Add your owndestination_typeby extendingattemptDispatchinsrc/router.js. - No streaming, no inbound rate-limiting. Put a reverse proxy in front if you expose the webhook to the public Internet.
- The static
BEARER_TOKEN-style API key comparison is not constant-time. Treat the host as a private appliance.
matheus@devops:~$ cat LICENSEMIT — see LICENSE.
matheus@devops:~$ contactmatheus@devops:~$ _