Pular para o conteúdo principal

Verificação da assinatura do webhook

A notificação de pagamento chega a um endereço que só a sua loja e o nosso serviço conhecem. Mas o endereço pode vazar — de logs, da configuração, do histórico de desenvolvimento. Para distinguir uma notificação genuína de uma falsificação, cada requisição é assinada com o segredo do webhook.

Verifique a assinatura antes de entregar o pedido ou mudar o status dele.

Cabeçalhos da assinatura

CabeçalhoValor
X-TimestampHorário de envio, unix time em segundos
X-SignatureO prefixo sha256= e o HMAC-SHA256 em hex

A string assinada é o horário de envio, um ponto e o corpo da requisição:

1756901234.{"wallet":{...},"project":{...},"invoice":{...},"payment":{...}}

A chave de assinatura é o campo Segredo do webhook (Webhook secret) nas configurações do projeto. Você pode reemiti-lo lá se o valor for comprometido.

Etapas de verificação

  1. Pegue o corpo bruto da requisição, antes do parse do JSON.
  2. Concatene o valor de X-Timestamp, um ponto e o corpo.
  3. Calcule o HMAC-SHA256 com o segredo do webhook.
  4. Compare o resultado com X-Signature usando uma comparação de tempo constante.
  5. Rejeite a requisição se X-Timestamp se desviar demais do horário atual.

Exemplos de implementação

<?php
$secret = 'webhook secret from your project settings';

$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

// Guards against a replayed request
if (abs(time() - (int)$timestamp) > 300) {
http_response_code(400);
exit;
}

$expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$body, $secret);

// hash_equals compares in constant time
if (!hash_equals($expected, $signature)) {
http_response_code(403);
exit;
}

$data = json_decode($body, true);

// Handle the order

http_response_code(200);

O que ter em mente

O corpo bruto é assinado. Se você fizer o parse do JSON e remontá-lo, a ordem das chaves e os espaços mudam — a assinatura não vai corresponder. Calcule o HMAC antes do parse.

Cada nova tentativa tem a sua própria assinatura. O horário de envio é novo a cada tentativa, então a assinatura também difere. Não guarde uma assinatura para comparar com requisições futuras.

O segredo pode ser reemitido. O segredo antigo para de funcionar imediatamente após a reemissão, então atualize o valor na sua loja no mesmo momento.

A verificação da assinatura não substitui a idempotência. A assinatura prova que a requisição é autêntica, não que você a está vendo pela primeira vez. Uma nova tentativa chega com uma assinatura válida — verifique pelo invoice.id.