- Números
- Teto diário
- Pagando
- Receita mensal
Instâncias
-
- Nenhuma instância nesta organização.
Conecta múltiplas contas, envia e recebe mensagens e entrega cada evento por webhook em tempo real, tudo numa stack Go enxuta.
Multi-instância: várias contas WhatsApp na mesma API, isoladas por token
Conversas ao vivo: histórico, mídia, respostas e reações direto no painel
Anti-bloqueio: proxy por instância, teto diário de envios e alerta de saúde do número
Webhooks & WebSocket: eventos em tempo real com HMAC opcional
Entre com seu email e senha.
Informe o email de acesso. Se ele estiver cadastrado, você recebe um link pra criar uma nova senha.
Escolha a nova senha do painel. Os outros aparelhos conectados vão precisar entrar de novo.
Atraso, teste acabando e instâncias com problema.
Nada pendente: nenhuma conta em atraso, teste acabando ou instância caída.
Nenhum assinante pagando ainda.
| Empresa | Situação | Plano | Números | Teto diário | Criada |
|---|---|---|---|---|---|
|
|
desligada no GO
sem organização no GO
|
Organização criada direto no GO, sem assinatura. Os limites vêm do cadastro no GO.
Carregando dados da conta…
Cancelar aqui também cancela a assinatura no Asaas.
Esta conta ainda não tem organização no GO. A sincronização cria a organização quando a conta tem acesso (teste válido ou pagamento em dia).
Termos aceitos pelo cliente.
Nenhum aceite registrado.
Nenhuma instância nesta empresa.
Mensalidades da assinatura no Asaas e cobranças avulsas.
| Vencimento | Descrição | Valor | Forma | Situação | Fatura |
|---|---|---|---|---|---|
|
avulsa
|
|
Fatura Recibo - |
Nenhuma cobrança ainda.
Carregando cobranças…
O que aconteceu nesta conta, do mais novo ao mais antigo.
Sem registros.
Carregando instâncias…
Totais de todos os assinantes; os filtros abaixo só mudam a tabela.
Carregando proxies…
Nenhum proxy cadastrado. Cole as URLs em "Adicionar proxies" para começar.
Nenhum proxy encontrado para essa busca.
| Ação | Empresa | Por | Detalhe | Quando |
|---|---|---|---|---|
Carregando atividade…
Nenhuma atividade registrada ainda.
| Ação | Alvo | Por | Detalhe | Quando |
|---|---|---|---|---|
Carregando auditoria…
Nenhum evento de auditoria ainda.
Leitura da configuração deste servidor.
Carregando…
Configuração indisponível nesta versão do GO.
Asaas, endereços e regras do teste.
Configuração indisponível nesta versão do portal.
Normalmente rodam no agendador. Aqui dá para rodar na hora.
Guarde agora: só aparece uma vez. É a credencial da org para integrações (n8n). O cliente entra no painel por email/senha.
A empresa de destino precisa estar ligada e ter vaga no limite de instâncias.
Use o mesmo número que vai parear. Ex.: 5511999999999 (Brasil + 11 + 9 9999-9999).
Digite este código no celular
No WhatsApp do número: Aparelhos conectados → Conectar com número de telefone e digite o código acima.
Aparência e comportamentos automáticos da instância.
Credencial desta instância pra integrações (header apikey).
Gerar um novo token invalida o atual na hora. Atualize as integrações que o usam.
Nome e avatar exibidos no painel.
PNG, JPG, WebP ou SVG. Até ~1.5 MB.
Aplicar encerra a conexão atual e reconecta pela saída escolhida, mantendo o pareamento. Com proxy, se ele cair a instância fica sem conexão (não sai pelo servidor).
Endpoints HTTP que recebem os eventos desta instância.
Integre seu sistema, n8n ou qualquer cliente HTTP. Corpo e respostas em JSON.
Toda chamada leva a credencial no header apikey (também vale Authorization: Bearer). Há duas credenciais:
Cria, lista e apaga as instâncias da sua org. Também opera qualquer instância dela. Fica no painel, em Configurações, Chave de API (lá também dá pra gerar uma nova).
Opera uma instância só: conexão, mensagens, conversas e webhooks. Vem na resposta do POST /instance, no botão Token do Dashboard e em Configurações gerais, onde também dá pra gerar um novo.
| {id} | Nas rotas, aceita o ID ou o nome da instância. |
| ?apikey= | Alternativa ao header onde não dá para mandar header: <img>, <audio>, <video> e WebSocket. |
| master key | Existe só para o administrador da plataforma e não entra nesta documentação. |
curl /instance \ -H "apikey: SUA_API_KEY"
Cria uma instância na sua org (a org vem da api_key). Nome até 60 caracteres, único na org. Respeita o limite de instâncias da org.
curl -X POST /instance \
-H "apikey: SUA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "minha-loja" }'
{
"id": "ba6bd24e-3870-42a9-b13e-5c1f0d2a7e11",
"org_id": "org_ab12...",
"name": "minha-loja",
"token": "7cf1cebce83c1208...",
"status": "disconnected",
"settings": { "reject_calls": false, "ignore_groups": false, "...": "..." },
"created_at": "2026-10-15T12:00:00Z",
"updated_at": "2026-10-15T12:00:00Z"
}
Erros: 403 quota_exceeded (limite da org), 409 conflict (nome já existe).
Lista as instâncias da org, com status real da conexão.
Detalhes, status e settings. last_event_at é o último evento recebido do WhatsApp (ajuda a ver conexão parada).
Desconecta, desfaz o pareamento e apaga a instância. Irreversível. Resposta 204.
Gera um novo token da instância e devolve a instância com ele. O token antigo para de valer na hora. Só com a api_key da org: o próprio token não troca a si mesmo (403).
Devolve a api_key da org: { "api_key": "..." }.
Gera uma nova api_key e devolve { "api_key": "..." }. A antiga para de valer na hora; os tokens das instâncias não mudam.
{ "name": "loja-sp" }Imagem da instância no painel. Aceita URL, data URI base64 (até 2 MB) ou preset. Vazio volta para as iniciais.
{ "avatar": "preset:flame" }
Atualização parcial: mande só o que muda, campos omitidos mantêm o valor atual. Devolve as settings completas já gravadas.
| reject_calls | Rejeita chamadas automaticamente. |
| ignore_groups | Descarta mensagens de grupo: não grava e não dispara evento. |
| always_online | Mantém a presença como online. |
| auto_read_messages | Marca as mensagens recebidas como lidas. |
| auto_read_status | Marca os status recebidos como vistos. |
| typing_simulation | Antes de cada envio mostra "digitando" (ou "gravando" no áudio) por um tempo humano. |
| typing_min_ms | Espera mínima em ms. 0 usa o padrão (900). |
| typing_max_ms | Espera máxima em ms. 0 usa o padrão (6000). |
| daily_message_limit | Fica gravado, mas não bloqueia envio. O teto que bloqueia é o diário da org, definido pelo administrador. |
{ "auto_read_messages": true, "typing_simulation": true }
Pareie por QR ou por código de 8 dígitos. Status possíveis: disconnected, connecting, qr, connected, logged_out.
Inicia a conexão e devolve o QR quando houver. Chame de novo até virar connected, ou escute o evento connection.qr.
{
"status": "qr",
"qr_code": "2@abc...",
"qr_image": "data:image/png;base64,iVBORw0KG..."
}
QR atual como imagem PNG, pronto para <img src> com ?apikey=. 404 no_qr se não houver QR.
Pareamento sem QR. No celular: Aparelhos conectados, Conectar com número de telefone, e digite o código.
{ "phone": "5511999999999" }
Resposta: { "code": "ABCD-WXYZ" }
Fecha a conexão e mantém o pareamento. /connect reconecta sem QR. Resposta 204.
Desfaz o pareamento. O próximo /connect gera QR novo. Resposta 204.
Campo to:
| 5511999999999 | Pessoa: só o número, com DDI e DDD, sem + nem espaços. |
| ...@g.us | Grupo: JID completo, ex.: 120363012345678901@g.us. |
| ...@lid / ...@newsletter | Também aceitos com o JID completo, assim como ...@s.whatsapp.net. |
Todo envio devolve:
{
"message_id": "3EB0C431D5F2A1B2C3D4",
"timestamp": "2026-10-15T12:00:00Z",
"status": "sent"
}
{
"to": "5511999999999",
"message": "Olá!",
"reply_to": "3EB0C431D5F2A1B2C3D4"
}
reply_to é opcional: ID da mensagem citada.
Corpo de mídia, igual em sendImage, sendVideo, sendAudio, sendDocument e sendSticker. Mande url ou base64.
| to | Destino (obrigatório). |
| url | URL pública do arquivo. O servidor baixa (até 64 MB). |
| base64 | Conteúdo em base64, com ou sem o prefixo data:...;base64,. |
| caption | Legenda (imagem, vídeo e documento). |
| filename | Nome do arquivo. Obrigatório no sendDocument. |
| mime_type | Opcional. Sem ele o tipo é detectado. |
| reply_to | Opcional: ID da mensagem citada. |
{
"to": "5511999999999",
"url": "https://exemplo.com/foto.jpg",
"caption": "Segue a foto"
}
Vídeo com legenda opcional. Corpo de mídia.
Vai como nota de voz (ptt) por padrão. Use ?ptt=false para mandar como arquivo de áudio. Corpo de mídia, sem caption.
Documento. filename obrigatório, caption opcional.
Figurinha em WebP. Corpo de mídia, sem caption.
{
"to": "5511999999999",
"latitude": -23.5505,
"longitude": -46.6333,
"name": "Loja Paulista",
"address": "Av. Paulista, 1000, São Paulo"
}
{
"to": "5511999999999",
"display_name": "Fulano",
"vcard": "BEGIN:VCARD\nVERSION:3.0\nFN:Fulano\nTEL:+5511988887777\nEND:VCARD"
}
{
"to": "5511999999999",
"name": "Qual horário?",
"options": ["Manhã", "Tarde", "Noite"],
"selectable_count": 1
}
Pelo menos 2 opções. selectable_count padrão 1.
Texto com prévia de link. Só to e text são obrigatórios.
{
"to": "5511999999999",
"text": "Veja: https://exemplo.com/oferta",
"title": "Oferta da semana",
"description": "Válida até domingo",
"thumbnail_url": "https://exemplo.com/thumb.jpg"
}
{
"to": "5511999999999",
"message_id": "3EB0C431D5F2A1B2C3D4",
"emoji": "\ud83d\udc4d",
"from_me": false
}
| emoji | O emoji da reação (no exemplo, escapado em JSON). Vazio remove a reação. |
| from_me | true se a mensagem alvo foi enviada por esta instância. |
| participant | Em grupo, quando a mensagem alvo é de outra pessoa: JID de quem enviou, ex.: 5511988887777@s.whatsapp.net. |
Edita o texto de uma mensagem enviada pela instância. O WhatsApp só aceita até cerca de 15 minutos após o envio.
{ "to": "5511999999999", "text": "Texto corrigido" }
Apaga para todos.
{ "to": "5511999999999" }
Marca como lidas. Em grupo, sender é o JID de quem enviou. Resposta 204.
{
"chat": "5511999999999@s.whatsapp.net",
"message_ids": ["3EB0C431D5F2A1B2C3D4"]
}
Histórico gravado de um chat, mais recentes primeiro.
| chat | JID do chat (obrigatório). |
| limit | Padrão 100, máximo 500. |
| before | RFC3339: mensagens anteriores a este instante (paginação). |
| since | RFC3339: só as alteradas depois deste instante, em ordem crescente (polling). |
[{
"id": "d1f0...",
"instance_id": "ba6bd24e-...",
"message_id": "3EB0C431D5F2A1B2C3D4",
"chat_jid": "5511999999999@s.whatsapp.net",
"sender_jid": "5511999999999@s.whatsapp.net",
"from_me": false,
"type": "text",
"content": "Olá!",
"status": "read",
"timestamp": "2026-10-15T12:00:00Z",
"created_at": "2026-10-15T12:00:01Z",
"updated_at": "2026-10-15T12:00:05Z"
}]
Uma mensagem gravada, no mesmo formato do histórico.
Busca por texto. Parâmetros: q (obrigatório), chat (opcional), limit (padrão 50, máximo 200). Cada resultado traz snippet com o termo em <mark>.
Tudo que o servidor gravou desde um instante, na ordem de gravação. Serve para recuperar o que chegou durante uma queda (fila offline e histórico do celular).
| since | RFC3339 (obrigatório): gravadas a partir deste instante. |
| min_timestamp | RFC3339: piso do horário da mensagem. Padrão = since, no máximo 31 dias atrás. |
| before | RFC3339: só gravadas antes deste instante. |
| cursor | next_cursor da página anterior. |
| limit | Padrão 100, máximo 500. |
| include_groups | true inclui grupos, canais e status. |
{
"messages": [{ "message_id": "3EB0...", "chat_jid": "5511999999999@s.whatsapp.net", "chat_lid": "123456789@lid", "chat_name": "Fulano", "...": "..." }],
"next_cursor": "MjAyNi0xMC0xNVQxMjowMDowMVo...",
"has_more": false
}
Baixa e descriptografa a mídia de uma mensagem. Devolve os bytes com o Content-Type certo. Aceita ?apikey= para usar direto em <img> ou <video>.
Erro sempre vem como:
{ "error": "daily_limit_reached", "message": "texto legível" }
| 400 bad_request | Corpo inválido, campo obrigatório faltando ou campo desconhecido no JSON. |
| 401 unauthorized | Credencial ausente ou inválida. |
| 403 forbidden | Token de outra instância ou organização inativa. |
| 404 not_found | Instância, mensagem ou webhook não encontrado (ou de outra org). |
| 429 daily_limit_reached | A instância atingiu o teto diário de envios definido para a org. Volta a enviar às 00:00 UTC. |
| 429 (sem JSON) | Mais de 300 requisições por minuto por IP nas rotas send*. |
| 503 not_connected | A instância não está rodando no servidor. Chame /connect. |
| 500 send_failed | O WhatsApp recusou o envio ou a instância está sem conexão. |
Conversas com a última mensagem e o total.
[{
"chat_jid": "5511999999999@s.whatsapp.net",
"name": "Fulano",
"last_message_id": "3EB0C431D5F2A1B2C3D4",
"last_content": "Valeu!",
"last_type": "text",
"last_from_me": false,
"last_timestamp": "2026-10-15T12:00:00Z",
"last_status": "read",
"total": 17,
"is_group": false,
"is_newsletter": false
}]
Nome de um chat (contato, grupo ou canal). ?chat=JID obrigatório. Resposta: { "chat_jid", "name" }.
URL da foto de perfil de um contato ou grupo. ?jid= obrigatório (número ou JID), ?preview=true para miniatura. Resposta: { "url" }.
Troca o recado da conta. Resposta 204.
{ "text": "Atendimento das 8h às 18h" }
Presença global: available ou unavailable. Resposta 204.
{ "state": "available" }
Mostra "digitando" ou "gravando áudio" num chat. state: composing ou paused. media: text ou audio (gravando). Resposta 204.
{ "to": "5511999999999", "state": "composing", "media": "text" }
{
"jid": "120363012345678901@g.us",
"owner_jid": "5511999999999@s.whatsapp.net",
"name": "Time vendas",
"topic": "Avisos da equipe",
"is_locked": false,
"is_announce": false,
"is_community": false,
"created_at": 1760540000000,
"participant_count": 2,
"participants": [
{ "jid": "5511999999999@s.whatsapp.net", "is_admin": true, "is_super_admin": true },
{ "jid": "5511988887777@s.whatsapp.net", "is_admin": false, "is_super_admin": false }
]
}
Link de convite: { "link": "https://chat.whatsapp.com/..." }. ?reset=true revoga o atual e gera outro.
Confere quais números têm WhatsApp.
{ "phones": ["5511999999999", "5511888888888"] }
Resposta:
[
{ "phone": "+5511999999999", "exists": true, "jid": "5511999999999@s.whatsapp.net" },
{ "phone": "+5511888888888", "exists": false }
]
O teto de números por chamada e por minuto vem da configuração do servidor: 400 too_many_numbers e 429 rate_limited (Retry-After: 60). Consulta em massa parece lista fria e pode banir o número: confira só quem vai receber mensagem.
{
"messages": 1024,
"messages_sent": 400,
"messages_received": 624,
"chats": 42,
"groups": 3,
"contacts": 1200,
"connected": true,
"status": "connected",
"last_message_at": "2026-10-15T12:00:00Z"
}
Mensagens por dia. ?days= padrão 7, máximo 365. Itens: day, total, sent, received.
Mensagens por dia da semana e hora. ?weeks= padrão 4, máximo 52. Itens: weekday (0 = domingo), hour, count.
Hoje contra ontem e semana contra semana anterior: today, yesterday, this_week, last_week, daily_delta_pct, week_delta_pct (-1 = sem base).
Últimos eventos de conexão. ?limit= padrão 30, máximo 100. Itens: event, detail, created_at.
Disponibilidade na janela. ?days= padrão 7, máximo 90. Campos: uptime_percent, reconnects, disconnects, last_disconnect_at, last_connect_at.
Avisos do WhatsApp (ban temporário, trava, restrição de contato novo) e falhas de envio, do mais novo ao mais antigo. ?limit= padrão 100, máximo 500. ?since= RFC3339.
[{
"id": 81,
"instance_id": "ba6bd24e-...",
"kind": "reachout_timelock",
"severity": "warning",
"detail": "proibido iniciar conversa nova",
"expires_at": "2026-10-16T12:00:00Z",
"created_at": "2026-10-15T12:00:00Z"
}]
Stream de todos os eventos da instância em tempo real, sem filtro, com os mesmos nomes dos webhooks. O token vai em ?apikey=.
const ws = new WebSocket('/ws/ID_DA_INSTANCIA?apikey=TOKEN');
ws.onmessage = (e) => console.log(JSON.parse(e.data));
{ "type": "ws.connected", "instance": "ba6bd24e-...", "timestamp": 1760540000000 }
{
"event": "message.received",
"instance_id": "ba6bd24e-...",
"instance_name": "minha-loja",
"timestamp": 1760540000000,
"payload": { "type": "message.received", "messageId": "3EB0...", "...": "mesmo corpo do webhook" }
}
O servidor manda ping a cada 30 s. Cliente lento é desconectado: reconecte. O WebSocket não traz media.base64.
URLs suas que recebem POST com os eventos da instância. Pode ter vários por instância, cada um com sua lista de eventos.
{
"name": "n8n produção",
"url": "https://n8n.seu-dominio.com/webhook/abc",
"events": ["message.received", "message.status", "connection.state"],
"secret": "segredo-para-hmac",
"enabled": true,
"webhook_by_events": false,
"webhook_base64": false
}
| url | http ou https, em endereço público. |
| events | Vazio assina tudo (all). Aceita curinga por prefixo, ex.: connection.*. |
| secret | Opcional. Com ele, cada POST vem assinado (veja Assinatura HMAC). |
| webhook_by_events | Soma /nome.do.evento ao fim da URL, ex.: .../webhook/abc/message.received. |
| webhook_base64 | message.received e message.sent passam a trazer media com o arquivo em base64. |
Lista os webhooks da instância.
Mesmos campos da criação. Mande só o que muda.
Remove o webhook. Resposta 204.
Histórico de entregas. Filtros: success (true ou false), event, limit (padrão 50, máximo 500), offset. Resposta: { items, total, limit, offset }.
Uma entrega com o payload enviado (até 8 KB) e a resposta do seu servidor (até 4 KB).
Reenvia o payload guardado para a URL atual. Uma tentativa, sem nova fila. Como o histórico guarda só 8 KB, entrega maior (ex.: com base64) volta cortada.
Manda um POST de teste (evento webhook.test). ?type=ctwa simula a primeira mensagem de um lead vindo de anúncio. Retry e teste respondem:
{ "success": true, "status_code": 200, "latency_ms": 132, "response": "ok" }
Saúde das entregas. ?since= é uma duração, padrão 24h (ex.: 168h para 7 dias). Campos: total, success, failed, success_rate (0 a 1, -1 sem entregas).
POST com JSON. Por padrão: timeout de 10 s e até 3 tentativas, com espera de 1 s e depois 3 s. Resposta 2xx ou 3xx conta como sucesso. Toda entrega fica no histórico.
Content-Type: application/json User-Agent: Ignitecore-GO-Webhook/0.1 X-Webhook-Event: message.received X-Webhook-Signature: sha256=<hex> (só com secret)
Para rotear, use o header X-Webhook-Event (ou event no WebSocket): ele sempre traz o nome desta lista. O campo type do corpo pode vir com nome antigo em alguns eventos.
Corpo plano. Todo evento traz type, instanceId, instanceName, connectedPhone e timestamp em milissegundos. Em grupo vem também participantPhone (quem enviou).
fromMe: true quando a mensagem saiu do celular ou de outro aparelho conectado. Envios pela API disparam message.sent. quotedStanzaId aparece quando a mensagem responde outra. offline: true quando chegou pela fila de recuperação após uma queda.
{
"type": "message.received",
"instanceId": "ba6bd24e-...",
"instanceName": "minha-loja",
"connectedPhone": "5511950448963",
"messageId": "3EB0C431D5F2A1B2C3D4",
"phone": "5511988887777",
"chatLid": "5511988887777@s.whatsapp.net",
"chatName": "Fulano",
"senderName": "Fulano",
"fromMe": false,
"timestamp": 1760540000000,
"status": "RECEIVED",
"isGroup": false,
"isNewsletter": false,
"isStatusReply": false,
"isEdit": false,
"offline": false,
"messageType": "text",
"text": { "message": "Olá!" }
}
A URL do WhatsApp vem criptografada. Para o arquivo, use GET /media/{id}/{message_id} ou ligue webhook_base64 (bloco media).
{
"type": "message.received",
"messageId": "3EB0C431D5F2A1B2C3D5",
"phone": "5511988887777",
"fromMe": false,
"timestamp": 1760540000000,
"messageType": "image",
"image": {
"caption": "Comprovante",
"mimeType": "image/jpeg",
"imageUrl": "https://mmg.whatsapp.net/...",
"thumbnail": null,
"width": 1080,
"height": 1920
},
"media": { "mimeType": "image/jpeg", "base64": "/9j/4AAQSkZJRg...", "size": 183402 }
}
| video | caption, mimeType, videoUrl, seconds (ptv: true no vídeo redondo) |
| audio | mimeType, audioUrl, seconds, ptt |
| document | mimeType, documentUrl, fileName, pageCount, caption |
| sticker | mimeType, stickerUrl |
| location | latitude, longitude, name, address |
| contact | displayName, vcard |
Quando o lead chega por um anúncio de clique para WhatsApp do Meta, a primeira mensagem dele vem com o bloco ad. As seguintes vêm sem. ctwaClid é a chave de atribuição do clique.
"ad": {
"title": "Plano Premium 50% OFF",
"body": "Aproveite enquanto durar",
"mediaType": "IMAGE",
"sourceType": "ad",
"sourceId": "120208123456789012",
"sourceApp": "facebook",
"ctwaClid": "ARBxYz...",
"ref": "campanha-black-friday",
"sourceUrl": "https://fb.me/...",
"thumbnailUrl": "https://scontent.../thumb.jpg",
"mediaUrl": "https://scontent.../ad.jpg",
"ctaPayload": "",
"clickToWhatsappCall": false,
"containsAutoReply": true,
"greetingMessageBody": "Olá! Como posso ajudar?",
"adType": "AD_TYPE_UNSPECIFIED",
"adPreviewUrl": "",
"websiteUrl": ""
}
messageId é a mensagem original. O texto novo vem em newContent e no bloco do tipo.
{
"type": "message.edited",
"messageId": "3EB0C431D5F2A1B2C3D4",
"editStanzaId": "3EB0D99E7A1F00112233",
"phone": "5511988887777",
"chatLid": "5511988887777@s.whatsapp.net",
"fromMe": false,
"isGroup": false,
"isEdit": true,
"messageType": "text",
"newContent": "Texto corrigido",
"text": { "message": "Texto corrigido" },
"timestamp": 1760540060000
}
Apagada para todos. fromMe diz se a mensagem apagada era desta instância.
{
"type": "message.deleted",
"messageId": "3EB0C431D5F2A1B2C3D4",
"revokeStanzaId": "3EB0E1A2B3C4D5E6F708",
"phone": "5511988887777",
"chatLid": "5511988887777@s.whatsapp.net",
"fromMe": false,
"isGroup": false,
"timestamp": 1760540120000
}
messageId é a mensagem que recebeu a reação. targetFromMe: true quando ela era desta instância. removed: true com reaction vazio quando a reação foi tirada.
{
"type": "message.reaction",
"messageId": "3EB0C431D5F2A1B2C3D4",
"reactionId": "3EB0F0E1D2C3B4A59687",
"phone": "5511988887777",
"chatLid": "5511988887777@s.whatsapp.net",
"fromMe": false,
"targetFromMe": true,
"isGroup": false,
"reaction": "\ud83d\udc4d",
"removed": false,
"timestamp": 1760540180000
}
Com secret, cada POST traz X-Webhook-Signature: sha256=<hex>, onde hex é o HMAC-SHA256 do corpo bruto com o secret. Calcule sobre os bytes recebidos, antes de fazer parse do JSON.
const crypto = require('crypto');
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const esperado = 'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
const recebido = req.get('X-Webhook-Signature') || '';
const ok = recebido.length === esperado.length &&
crypto.timingSafeEqual(Buffer.from(recebido), Buffer.from(esperado));
if (!ok) return res.sendStatus(401);
const evento = JSON.parse(req.body);
// req.get('X-Webhook-Event') diz qual evento chegou
res.sendStatus(200);
});