Validar a assinatura do webhook em Node.js, PHP e Python
Objetivo
Seção intitulada “Objetivo”Ao final, o seu servidor vai recusar qualquer chamada de webhook que não tenha sido assinada pela API. Sem isso, qualquer pessoa que descubra o seu endereço poderia enviar eventos falsos.
Como a assinatura funciona
Seção intitulada “Como a assinatura funciona”Cada chamada traz o cabeçalho x-nigora-signature, no formato sha256=<hex>. O valor é o HMAC-SHA256 dos bytes exatos do corpo, calculado com o segredo da instância e escrito em hexadecimal.
Então o seu servidor faz o mesmo cálculo e compara o resultado com o cabeçalho. Duas regras:
- Use o corpo bruto, como chegou. Se você ler o JSON e serializar de novo, os bytes mudam e a assinatura deixa de bater.
- Compare em tempo constante. Uma comparação comum (
==) pode vazar, pelo tempo de resposta, quantos caracteres estavam certos.
Pré-requisitos
Seção intitulada “Pré-requisitos”- Um servidor que já recebe webhooks, como o do tutorial Receber mensagens por webhook.
- O segredo da instância. Leia-o com
GET /v1/instances/{instanceId}/webhooks: ele vem no camposecret.
curl https://api.zap.nigora.com.br/v1/instances/INSTANCIA_ID/webhooks \ -H "Authorization: Bearer TOKEN"Guarde o segredo numa variável de ambiente, nunca no código:
export NIGORA_SEGREDO_DO_WEBHOOK="cole-aqui-o-secret"1. Escolha a sua linguagem
Seção intitulada “1. Escolha a sua linguagem”Os três exemplos abaixo são servidores completos. Cada um responde 401 quando a assinatura não bate e 200 quando bate.
Node.js
Seção intitulada “Node.js”Salve como validar.mjs:
import { createHmac, timingSafeEqual } from 'node:crypto'import { createServer } from 'node:http'
const segredo = process.env.NIGORA_SEGREDO_DO_WEBHOOKif (!segredo) { console.error('Defina NIGORA_SEGREDO_DO_WEBHOOK.') process.exit(1)}
// `corpoBruto` é um Buffer: o corpo exatamente como chegou.function assinaturaValida(corpoBruto, cabecalho) { const esperada = 'sha256=' + createHmac('sha256', segredo).update(corpoBruto).digest('hex') const a = Buffer.from(esperada) const b = Buffer.from(cabecalho ?? '') return a.length === b.length && timingSafeEqual(a, b)}
createServer((req, res) => { const pedacos = [] req.on('data', (pedaco) => pedacos.push(pedaco)) req.on('end', () => { const corpoBruto = Buffer.concat(pedacos) if (!assinaturaValida(corpoBruto, req.headers['x-nigora-signature'])) { res.writeHead(401).end() return } res.writeHead(200).end('ok') console.log('Assinatura válida:', req.headers['x-nigora-event']) })}).listen(3000, () => console.log('Ouvindo na porta 3000'))Execute com node validar.mjs.
Salve como validar.php:
<?php$segredo = getenv('NIGORA_SEGREDO_DO_WEBHOOK');if (!$segredo) { http_response_code(500); exit;}
// O corpo bruto, exatamente como chegou.$corpo = file_get_contents('php://input');$recebida = $_SERVER['HTTP_X_NIGORA_SIGNATURE'] ?? '';$esperada = 'sha256=' . hash_hmac('sha256', $corpo, $segredo);
if (!hash_equals($esperada, $recebida)) { http_response_code(401); exit;}
http_response_code(200);error_log('Assinatura válida: ' . ($_SERVER['HTTP_X_NIGORA_EVENT'] ?? ''));echo 'ok';Execute com php -S 127.0.0.1:3000 validar.php.
Salve como validar.py:
import hashlibimport hmacimport osfrom http.server import BaseHTTPRequestHandler, HTTPServer
segredo = os.environ["NIGORA_SEGREDO_DO_WEBHOOK"].encode("utf-8")
def assinatura_valida(corpo_bruto: bytes, cabecalho: str) -> bool: esperada = "sha256=" + hmac.new(segredo, corpo_bruto, hashlib.sha256).hexdigest() return hmac.compare_digest(esperada.encode("utf-8"), cabecalho.encode("utf-8"))
class Receptor(BaseHTTPRequestHandler): def do_POST(self): corpo_bruto = self.rfile.read(int(self.headers.get("Content-Length", 0))) if not assinatura_valida(corpo_bruto, self.headers.get("x-nigora-signature", "")): self.send_response(401) self.end_headers() return self.send_response(200) self.end_headers() self.wfile.write(b"ok") print("Assinatura válida:", self.headers.get("x-nigora-event"))
HTTPServer(("127.0.0.1", 3000), Receptor).serve_forever()Execute com python3 validar.py.
2. Teste na sua máquina
Seção intitulada “2. Teste na sua máquina”Com o servidor rodando, simule uma chamada assinada. O comando calcula a assinatura com openssl:
CORPO='{"event":"message.received","instanceId":"teste","timestamp":"2026-11-02T14:30:00.000Z","data":{}}'ASSINATURA="sha256=$(printf '%s' "$CORPO" | openssl dgst -sha256 -hmac "$NIGORA_SEGREDO_DO_WEBHOOK" -hex | sed 's/^.* //')"
curl -i -X POST http://127.0.0.1:3000/ \ -H "Content-Type: application/json" \ -H "x-nigora-event: message.received" \ -H "x-nigora-signature: $ASSINATURA" \ -d "$CORPO"Resultado esperado: HTTP/1.1 200 OK e, no terminal do servidor, Assinatura válida: message.received.
Agora mude uma letra do corpo e repita só o curl, sem recalcular a assinatura. Resultado esperado: HTTP/1.1 401.
Se algo der errado
Seção intitulada “Se algo der errado”- Sempre
401: confira se o segredo é o da instância certa e se o seu framework não está lendo o JSON antes de você pegar o corpo bruto. - Troca de segredo: depois de
POST /v1/instances/{instanceId}/webhooks/secret/regenerate, o segredo antigo deixa de valer. Atualize a variável de ambiente.
Próximos passos
Seção intitulada “Próximos passos”- Webhooks: eventos, tentativas, histórico e reenvio.
- Limites e erros: formato dos erros da API.
- Conectar por código de pareamento: conecte o número sem usar a câmera.
Receber mensagens do WhatsApp por webhook Próximo
Conectar o número ao WhatsApp por código de pareamento