Como integrar a API do WhatsApp no n8n
Enviar mensagem no n8n é um nó HTTP Request com dois headers. Receber é um nó Webhook. A parte que ninguém conta é a do meio: validar a assinatura da entrega não tem nó pronto — precisa de um Code. Este guia mostra os três, e termina num fluxo que responde sozinho a quem manda mensagem.
O que você precisa
- Uma chave de API (
hub_pk_...) com o escopomessages:send, criada em /painel/chaves. Para criar o endpoint de webhook pela API, a chave também precisa dewebhooks:write. - O id do canal, de
GET /api/v1/channels. - Uma instância do n8n acessível pela internet, em HTTPS. O CRPRO Hub recusa URL de webhook que aponte para loopback, link-local ou faixa privada de IP —
400 WEBHOOK_URL_NOT_ALLOWED. Um n8n emlocalhostnão serve como destino: exponha por túnel ou publique. - O segredo do webhook (
whsec_...), devolvido uma única vez na criação do endpoint.
Enviar com o nó HTTP Request
Um nó só. A configuração inteira:
- Method:
POST. - URL:
https://crprohub.com/api/v1/channels/{id}/messages, com o id do canal no lugar de{id}. - Send Headers ligado, com
Authorization=Bearer hub_pk_...eIdempotency-Key— o valor deste segundo é o assunto da próxima seção. - Send Body ligado, Body Content Type em
JSON, Specify Body emUsing JSON.
{
"to": "5521999999999",
"type": "text",
"text": { "body": "Olá" }
}O to é o número do destinatário com código do país, só dígitos — sem +, sem espaço, sem parênteses. O type aceita text, image, audio, video, document, sticker, location, contacts, reaction, interactive e template, e cada um exige o objeto de mesmo nome ao lado. Não existe endpoint separado por tipo: é sempre este, mudando o type.
A resposta é 202, e isso não é detalhe: significa aceita e enfileirada para a Meta, não entregue. O nó HTTP Request trata 202 como sucesso, então o fluxo segue normalmente — mas um IF depois dele comparando o status com 200 nunca vai casar. O corpo vem envelopado em data: o identificador está em {{ $json.data.message_id }}.
hub_ch_...), que autentica este envio e só serve para o canal que o emitiu.Ligue também Include Response Headers and Status nas opções do nó. É o que traz o x-request-id de volta, e é ele que liga a sua execução ao registro em /painel/logs quando algo dá errado.
A Idempotency-Key no n8n
O header Idempotency-Key é obrigatório neste endpoint. A regra é simples de enunciar e fácil de errar no n8n: o valor precisa ser o mesmo em toda tentativa do mesmo envio e diferente entre envios distintos.
Quando o envio nasce de um evento — uma mensagem que chegou, um pedido que mudou de estado — a melhor chave não vem do n8n, vem do próprio evento:
resposta-{{ $json.evento.data.message_id }}Essa chave é estável de verdade. Se a entrega do webhook for reenviada, se você reexecutar a execução à mão, ou se o fluxo rodar duas vezes por qualquer motivo, o valor é o mesmo — e a API devolve a resposta original em vez de mandar a mensagem de novo. Nenhuma expressão de runtime do n8n te dá essa garantia.
Quando não há evento de origem — um disparo agendado, uma planilha, um formulário — use a execução:
crpro-{{ $execution.id }}-{{ $itemIndex }}Três coisas nessa expressão são deliberadas. $execution.id é único por execução e não muda quando o Retry On Fail do próprio nó tenta de novo — que é exatamente o comportamento que a idempotência precisa. $itemIndex separa os itens: sem ele, um nó que processa dez contatos manda os dez com a mesma chave e corpos diferentes, e do segundo em diante a resposta é 409 IDEMPOTENCY_KEY_REUSED. E o prefixo existe porque um id de execução novinho pode ter três ou quatro caracteres — curto demais, e a API responde 400 IDEMPOTENCY_KEY_INVALID.
Generate e tipo UUID gera um por item, num campo que você nomeia — depois referencie esse campo no header. Só entenda o que muda: aleatório por item sobrevive à retentativa do nó HTTP Request, mas não sobrevive a uma reexecução do fluxo inteiro, porque o Crypto roda de novo e gera outro. Chave derivada do dado é sempre mais forte que chave gerada na hora.Reexecutar com a mesma chave enquanto a primeira ainda está em curso responde 409 OPERATION_IN_PROGRESS — espere e repita, a resposta original volta quando a operação terminar. A tabela completa está em erros.
Receber com o nó Webhook
Do lado do recebimento, o nó Webhook do n8n é o destino. Configure HTTP Method em POST, escolha um Path, e ligue duas opções que decidem se a integração vai funcionar:
- Raw Body — sem isso o n8n faz parse do JSON e entrega um objeto, jogando os bytes originais fora. A assinatura cobre esses bytes exatos, então
JSON.stringifydo objeto produz outro HMAC e a verificação falha em 100% das entregas, sem nenhuma pista de que a causa é o parser. - Respond em
Using 'Respond to Webhook' Node. O endpoint tem 10 segundos para responder2xx; salvar no banco, chamar uma IA e mandar a resposta não cabe nesse orçamento. Com um nó Respond to Webhook logo depois da verificação, você responde rápido e continua processando no mesmo fluxo.
O n8n dá duas URLs para o mesmo nó: a de teste e a de produção. Registre a de produção — a URL de teste só escuta enquanto você mantém o botão de escuta ativo no editor, e uma entrega que chega depois disso falha. Uma entrega que falha é reenviada até nove vezes, somando dez tentativas, e depois de 20 falhas consecutivas o endpoint pausa sozinho.
curl -X POST https://crprohub.com/api/v1/webhooks \
-H "Authorization: Bearer hub_pk_EXEMPLO_NAO_REAL" \
-H "Content-Type: application/json" \
-d '{"name":"n8n","url":"https://SEU-N8N/webhook/crprohub","event_types":["message.received","message.status"]}'O event_types nasce vazio e endpoint sem evento assinado recebe todos os eventos, não nenhum. Os dois que interessam num fluxo de atendimento são message.received e message.status. O secret (whsec_...) vem só nesta resposta e não é reemitido. Você também pode criar o endpoint pelo painel — o passo a passo está em configurar webhook.
Verificar a assinatura exige um Code
Aqui acaba o “sem código”, e é melhor dizer isso na cara: não existe nó do n8n que valide a assinatura HMAC das entregas. Ou você escreve um Code, ou o seu webhook aceita qualquer POST que alguém mandar para aquela URL — e a URL de um nó Webhook é pública.
Toda entrega chega com X-Hub-Delivery-Id, X-Hub-Timestamp, X-Hub-Signature-Version e X-Hub-Signature-256, este último no formato sha256=HEX. No formato nativo (v2, o padrão) a assinatura não cobre só o corpo: cobre `${timestamp}.${delivery_id}.${corpo}`. Amarrar os três é o que impede transplantar um corpo capturado para outra entrega — e assinar só o corpo é o erro que mais derruba integração nova.
const { createHmac, timingSafeEqual } = require('crypto')
const entrada = $input.first()
const headers = entrada.json.headers
// A opcao Raw Body do no Webhook precisa estar ligada. Sem ela o n8n entrega
// o corpo ja convertido em objeto, e reserializar esse objeto nao devolve os
// mesmos bytes -- espacamento, ordem das chaves e escapes de Unicode mudam.
const rawBody = Buffer.from(entrada.binary.data.data, 'base64').toString('utf8')
const timestamp = headers['x-hub-timestamp']
const deliveryId = headers['x-hub-delivery-id']
const assinado = `${timestamp}.${deliveryId}.${rawBody}`
const esperada = createHmac('sha256', $env.CRPRO_WEBHOOK_SECRET).update(assinado).digest('hex')
const recebida = (headers['x-hub-signature-256'] ?? '').replace('sha256=', '')
const valido =
/^[0-9a-f]{64}$/.test(recebida) &&
timingSafeEqual(Buffer.from(recebida, 'hex'), Buffer.from(esperada, 'hex'))
return [{ json: { valido, deliveryId, evento: JSON.parse(rawBody) } }]Duas linhas desse trecho não são enfeite:
timingSafeEqual, nunca===. Comparação de string comum para no primeiro byte diferente, e esse tempo vaza quantos bytes o atacante já acertou — dá para descobrir a assinatura correta por tentativa e erro.- O teste
/^[0-9a-f]{64}$/antes doBuffer.from. Um valor de 64 caracteres não hexadecimais vira um buffer mais curto, etimingSafeEquallançaRangeErrorem vez de devolverfalse— o nó falha, o n8n devolve erro, e a entrega volta na retentativa.
NODE_FUNCTION_ALLOW_BUILTIN permitir — inclua crypto, escrito exatamente assim: a allowlist casa pelo especificador literal, então require('node:crypto') não resolve mesmo com crypto liberado. No n8n Cloud essa variável não é configurável, e o caminho não existe. Ler o segredo com $env depende de o acesso a variáveis de ambiente não estar bloqueado na instância. Se a sua não permite nenhum dos dois, você não tem como verificar a assinatura em tempo constante dentro do n8n: ponha um serviço seu na frente e mande para o n8n só o que já foi validado.Leia o X-Hub-Signature-Version antes de suspeitar do segredo. Um endpoint criado com delivery_format: "evohub" manda evohub-v1 e assina somente o corpo cru — cálculo diferente, mesmo sintoma. O trecho desse caso está em webhooks; em integração nova, prefira native.
O fluxo de resposta automática
Mensagem chega, o fluxo responde. Seis nós, nesta ordem:
- Webhook — POST, Raw Body ligado, Respond em
Using 'Respond to Webhook' Node. - Code — o trecho da seção anterior. Devolve
valido,deliveryIdeevento. - IF —
{{ $json.valido }}é verdadeiro. O ramo falso vai direto para um Respond to Webhook com status401. - Respond to Webhook — status
200, sem corpo. Fica antes do resto de propósito: o relógio de 10 segundos para aqui, e o que vem depois pode demorar o que precisar. - Remove Duplicates — no modo que lembra valores de execuções anteriores, com
{{ $json.deliveryId }}como chave. Uma retentativa reenvia o mesmoX-Hub-Delivery-Id; sem esse nó, o contato recebe a mesma resposta de novo a cada reenvio. - HTTP Request — o envio, com o corpo abaixo.
{
"to": "{{ $json.evento.data.from }}",
"type": "text",
"text": { "body": "Recebemos sua mensagem. Já respondemos por aqui." }
}Para as expressões funcionarem dentro do campo JSON, o campo inteiro precisa estar em modo expressão — o botão de alternância ao lado dele. Sem isso o n8n manda {{ $json.evento.data.from }} como texto literal, e o envio volta 400 INVALID_MESSAGE.
O Idempotency-Key deste nó é a expressão derivada do evento, lá de cima: resposta-{{ $json.evento.data.message_id }}. Junto com o Remove Duplicates, são duas camadas contra o mesmo problema — o nó protege dentro do seu n8n, a chave protege na API, e uma cobre a falha da outra.
Antes de mandar a resposta, vale filtrar por {{ $json.evento.event }} igual a message.received: se o endpoint também assina message.status, todo evento de status cairia no mesmo caminho e viraria uma resposta ao contato. O payload completo de message.received está em receber mensagens.
message.received a janela está sempre aberta — mas o mesmo nó HTTP Request usado num disparo agendado precisa de type: "template". Veja enviar template.Onde as pessoas erram
- Raw Body desligado. Sintoma:
validofalso em toda entrega, incluindo owebhook.test, com o segredo certo. É o primeiro lugar para olhar. - URL de teste registrada como endpoint. Funciona enquanto o editor está escutando e para de funcionar quando você fecha a aba. Vinte falhas seguidas pausam o endpoint, e aí nem a URL certa recebe até você reativar no painel.
- n8n em rede privada.
http://localhost:5678e IPs internos são recusados na criação com400 WEBHOOK_URL_NOT_ALLOWED. A entrega também não segue redirect: a URL cadastrada precisa ser o destino final. - IF esperando 200. O envio responde
202, sempre. Um IF comparando com200manda todo envio bem-sucedido para o ramo de erro. - Mesma chave para itens diferentes. Um nó HTTP Request que roda sobre uma lista sem
$itemIndexna chave manda o primeiro item e recebe409 IDEMPOTENCY_KEY_REUSEDem todos os outros. - Esperar 403 e receber 404. Um canal de outra organização responde
404, não403: 403 confirmaria que o recurso existe. Se o id parece certo e volta 404, confira a organização da chave antes do id. - Header com maiúsculas no Code. Os nomes chegam em minúsculas:
headers['x-hub-signature-256']funciona,headers['X-Hub-Signature-256']devolveundefinede a verificação falha sem erro aparente.
Os limites de taxa estão em limites — o envio tem um limite próprio por canal, e um fluxo do n8n que dispara em laço encosta nele rápido. O contrato dos endpoints está em referência de mensagens.