Pular para o conteúdo
Nigora Zap API

    Entrar Criar conta

    Validar a assinatura do webhook em Node.js, PHP e Python

    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.

    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:

    1. Use o corpo bruto, como chegou. Se você ler o JSON e serializar de novo, os bytes mudam e a assinatura deixa de bater.
    2. Compare em tempo constante. Uma comparação comum (==) pode vazar, pelo tempo de resposta, quantos caracteres estavam certos.
    • 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 campo secret.
    Terminal window
    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:

    Terminal window
    export NIGORA_SEGREDO_DO_WEBHOOK="cole-aqui-o-secret"

    Os três exemplos abaixo são servidores completos. Cada um responde 401 quando a assinatura não bate e 200 quando bate.

    Salve 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'))

    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 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()

    Execute com python3 validar.py.

    Com o servidor rodando, simule uma chamada assinada. O comando calcula a assinatura com openssl:

    Terminal window
    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.

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