Validar a assinatura dos webhooks
Confira o cabeçalho X-Signature (HMAC-SHA256 do corpo com o segredo do webhook) com exemplos em PHP e Node.js.
2 min de leitura Avançado Atualizado em 11 de novembro de 2026
Nesta página
Qualquer pessoa que descubra a URL do seu webhook pode enviar chamadas falsas para ela. Por isso, cada entrega do
ODSign vem assinada: antes de confiar no conteúdo, o seu sistema deve recalcular a assinatura e compará-la com a que
chegou no cabeçalho X-Signature.
Como a assinatura é feita
- Algoritmo: HMAC-SHA256.
- Chave: o segredo completo do webhook, exatamente como foi exibido ao criar, incluindo o prefixo
whsec_. - Mensagem: o corpo da requisição, byte a byte, exatamente como chegou.
- Formato: hexadecimal em letras minúsculas (64 caracteres), sem prefixo.
- Cabeçalho:
X-Signature. O nome do evento vem emX-Event.
O teste do botão Testar (evento test.ping) é assinado da mesma forma, então serve para conferir a sua
implementação. O corpo do teste é curto, por exemplo {"event":"test.ping","sent_at":"2026-11-11T13:00:00+00:00"},
sem os dados de envelope.
Atenção: Calcule a assinatura sobre o corpo bruto, antes de qualquer conversão. Se você decodificar o JSON e codificar de novo, espaços, acentos e barras podem mudar e a assinatura não vai bater.
Passo a passo
- Ao criar o webhook em Configurações › Webhooks, copie o segredo do aviso Copie o segredo do webhook agora (veja Webhooks de saída).
- Guarde o segredo no seu servidor, em variável de ambiente ou cofre de segredos (nos exemplos,
ODSIGN_WEBHOOK_SECRET). - No endpoint que recebe as chamadas, leia o corpo bruto e o cabeçalho
X-Signature. - Calcule o HMAC-SHA256 do corpo com o segredo e compare com o cabeçalho usando uma comparação em tempo constante.
- Se não bater, responda
401e descarte a chamada. Se bater, responda200e processe o evento. - Clique em Testar no cartão do webhook e confira se aparece "Teste entregue (HTTP 200).".
Exemplo em PHP
<?php
// webhook-odsign.php
$secret = getenv('ODSIGN_WEBHOOK_SECRET'); // "whsec_..." completo
$body = file_get_contents('php://input'); // corpo bruto, sem decodificar
$received = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $body, $secret);
if (! hash_equals($expected, $received)) {
http_response_code(401);
exit('assinatura inválida');
}
$payload = json_decode($body, true);
$event = $_SERVER['HTTP_X_EVENT'] ?? $payload['event'];
if ($event === 'envelope.completed') {
$envelopeId = $payload['envelope']['id'];
// Coloque o processamento pesado (por exemplo, baixar o PDF pela API) em uma fila.
}
http_response_code(200);
echo 'ok';
No Laravel, a mesma conferência fica assim:
use Illuminate\Http\Request;
Route::post('/webhooks/odsign', function (Request $request) {
$expected = hash_hmac('sha256', $request->getContent(), config('services.odsign.webhook_secret'));
abort_unless(hash_equals($expected, (string) $request->header('X-Signature')), 401);
$payload = $request->json()->all();
// ...
return response()->noContent(); // 204, também é 2xx
});
Lembre-se de tirar essa rota da verificação de CSRF do Laravel.
Exemplo em Node.js (Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
const SECRET = process.env.ODSIGN_WEBHOOK_SECRET; // "whsec_..." completo
// express.raw mantém o corpo como Buffer, sem converter o JSON.
app.post('/webhooks/odsign', express.raw({ type: 'application/json' }), (req, res) => {
const received = Buffer.from(req.get('X-Signature') || '', 'utf8');
const expected = Buffer.from(
crypto.createHmac('sha256', SECRET).update(req.body).digest('hex'),
'utf8',
);
if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
return res.status(401).send('assinatura inválida');
}
const payload = JSON.parse(req.body.toString('utf8'));
const event = req.get('X-Event') || payload.event;
if (event === 'envelope.completed') {
// Agende o download do PDF assinado pela API.
}
res.sendStatus(200);
});
app.listen(3000);
Use express.raw só nesta rota. Se a aplicação já usa express.json() antes dela, o corpo chega convertido e a
assinatura não confere.
Boas práticas
- Compare em tempo constante (
hash_equalsno PHP,crypto.timingSafeEqualno Node.js), nunca com==ou===. - Responda rápido. Devolva 2xx assim que validar e processe o evento em segundo plano. Respostas lentas contam como falha e geram novas tentativas.
- Trate repetições. A mesma entrega pode chegar mais de uma vez. Use o
event, oenvelope.ide orecipient.idpara ignorar o que já foi processado. - Descarte eventos muito antigos, se quiser uma proteção a mais contra reenvio malicioso: o campo
sent_atdo corpo faz parte do conteúdo assinado. - Não confie só no webhook para dados finais. Para decidir algo importante, consulte o envelope pela API
(Referência dos endpoints) com o
idrecebido. - Proteja o segredo como uma senha. Se ele vazar, exclua o webhook e crie outro, que terá um novo segredo.
Problemas comuns
- A assinatura nunca confere. Quase sempre o corpo foi alterado antes do cálculo (JSON decodificado e codificado de novo, espaços removidos, conversão de codificação). Calcule sobre o corpo bruto.
- Confere no teste, mas não nos eventos reais. Os eventos reais têm acentos e barras sem escape, como em
"Contrato de prestação". Confira se o seu framework não está convertendo o corpo antes da validação. - Uso o segredo sem o prefixo
whsec_. O prefixo faz parte da chave. Use o valor completo. - Perdi o segredo. Ele não pode ser exibido de novo. Exclua o webhook e crie outro em Configurações › Webhooks.
Este artigo foi útil?
Artigos relacionados
- Webhooks de saídaReceba no seu sistema um aviso a cada envio, visualização, assinatura, recusa, anulação e conclusão de envelopes.
- Visão geral da API RESTO que a API do ODSign faz, como autenticar, formatos de dados, limites de uso, códigos de erro e o fluxo típico de integração.
- Boas práticas de segurança da contaProteja a sua conta do ODSign com senha forte, celular confirmado, usuários com o papel certo e cuidado com tokens e mensagens falsas.