Ir al contenido
Nigora Zap API

    Entrar Crear cuenta

    Validar la firma del webhook en Node.js, PHP y Python

    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.

    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:

    1. 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.
    2. Compare en tiempo constante. Una comparación común (==) puede filtrar, por el tiempo de respuesta, cuántos caracteres eran correctos.
    • 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 campo secret.
    Ventana de terminal
    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:

    Ventana de terminal
    export NIGORA_SEGREDO_DO_WEBHOOK="cole-aqui-o-secret"

    Los tres ejemplos siguientes son servidores completos. Cada uno responde 401 cuando la firma no coincide y 200 cuando coincide.

    Guárdelo como validar.mjs:

    import { createHmac, timingSafeEqual } from 'node:crypto'
    import { createServer } from 'node:http'
    const segredo = process.env.NIGORA_SEGREDO_DO_WEBHOOK
    if (!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 hashlib
    import hmac
    import os
    from 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.

    Con el servidor en marcha, simule una llamada firmada. El comando calcula la firma con openssl:

    Ventana de terminal
    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.

    • 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.