Validar la firma del webhook en Node.js, PHP y Python
Objetivo
Sección titulada «Objetivo»Al final, su servidor rechazará cualquier llamada de webhook que no haya sido firmada por la API. Sin eso, cualquier persona que descubra su dirección podría enviar eventos falsos.
Cómo funciona la firma
Sección titulada «Cómo funciona la firma»Cada llamada trae el encabezado x-nigora-signature, en el formato sha256=<hex>. El valor es el HMAC-SHA256 de los bytes exactos del cuerpo, calculado con el secreto de la instancia y escrito en hexadecimal.
Entonces su servidor hace el mismo cálculo y compara el resultado con el encabezado. Dos reglas:
- Use el cuerpo sin procesar, tal como llegó. Si usted lee el JSON y lo serializa de nuevo, los bytes cambian y la firma deja de coincidir.
- Compare en tiempo constante. Una comparación común (
==) puede filtrar, por el tiempo de respuesta, cuántos caracteres eran correctos.
Requisitos previos
Sección titulada «Requisitos previos»- Un servidor que ya recibe webhooks, como el del tutorial Recibir mensajes por webhook.
- El secreto de la instancia. Léalo con
GET /v1/instances/{instanceId}/webhooks: viene en el camposecret.
curl https://api.zap.nigora.com.br/v1/instances/INSTANCIA_ID/webhooks \ -H "Authorization: Bearer TOKEN"Guarde el secreto en una variable de entorno, nunca en el código:
export NIGORA_SEGREDO_DO_WEBHOOK="cole-aqui-o-secret"1. Elija su lenguaje
Sección titulada «1. Elija su lenguaje»Los tres ejemplos siguientes son servidores completos. Cada uno responde 401 cuando la firma no coincide y 200 cuando coincide.
Node.js
Sección titulada «Node.js»Guárdelo 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'))Ejecute con node validar.mjs.
Guárdelo 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';Ejecute con php -S 127.0.0.1:3000 validar.php.
Guárdelo 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()Ejecute con python3 validar.py.
2. Pruebe en su máquina
Sección titulada «2. Pruebe en su máquina»Con el servidor en marcha, simule una llamada firmada. El comando calcula la firma con 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 y, en la terminal del servidor, Assinatura válida: message.received.
Ahora cambie una letra del cuerpo y repita solo el curl, sin recalcular la firma. Resultado esperado: HTTP/1.1 401.
Si algo sale mal
Sección titulada «Si algo sale mal»- Siempre
401: revise que el secreto sea el de la instancia correcta y que su framework no esté leyendo el JSON antes de que usted tome el cuerpo sin procesar. - Cambio de secreto: después de
POST /v1/instances/{instanceId}/webhooks/secret/regenerate, el secreto anterior deja de valer. Actualice la variable de entorno.
Próximos pasos
Sección titulada «Próximos pasos»- Webhooks: eventos, reintentos, historial y reenvío.
- Límites y errores: formato de los errores de la API.
- Conectar por código de emparejamiento: conecte el número sin usar la cámara.
Recibir mensajes de WhatsApp por webhook Siguiente página
Conectar el número a WhatsApp con código de emparejamiento