Como verificar um webhook do Qvard
Um guia rápido sobre o cabeçalho X-Qvard-Signature: o que é e como verificá-lo com algumas linhas de código.
O que é uma assinatura
Quando o status de um pagamento muda, o Qvard envia uma notificação (webhook) para o endereço nas suas configurações de loja. Qualquer pessoa pode enviar uma solicitação para esse endereço, por isso você precisa de uma forma de distinguir uma notificação real de uma falsa.
A assinatura é essa prova: um código curto que o Qvard calcula a partir da mensagem e da sua chave secreta. Apenas Qvard e você conhecem a chave, portanto apenas Qvard pode produzir o código correto.
O que você recebe
Todo webhook traz um cabeçalho extra e um pequeno corpo JSON:
X-Qvard-Signature: t=1760950000,v1=3f2a…c91e
{"id":"order_1024","status":"paid"}t é a hora em que a mensagem foi enviada (Unix, segundos). v1 é o código. Nas 24 horas seguintes à rotação da sua chave secreta, o cabeçalho traz dois valores v1: um para a chave nova e outro para a antiga.
Os campos id e status continuam como antes. Ao mesmo corpo assinado são adicionados quatro campos de valor:
- amount e currency: o valor e a moeda, como você os enviou na requisição.
- amountUsd: o valor contábil em USD.
- rate: unidades da moeda por 1 USD no momento em que o pagamento foi criado.
- As integrações antigas continuam funcionando: elas leem id e status e ignoram os campos novos. A assinatura é verificada sobre o mesmo corpo de antes.
Como verificar
- 1Pegue o corpo da solicitação exatamente como chegou: o texto bruto, antes de qualquer análise JSON.
- 2Leia t e cada v1 do cabeçalho X-Qvard-Signature.
- 3Calcule HMAC-SHA256 do texto t + "." + corpo com sua chave secreta (qvard_sk_…) e escreva em hexadecimal minúsculo.
- 4Se seu resultado corresponder a qualquer v1 (compare em tempo constante), a mensagem é genuína.
- 5Verifique se t não é mais velho do que 5 minutos, para que uma mensagem antiga não possa ser repetida.
- 6Apenas então use os dados e confirme o valor pago para esse pedido em seu próprio banco de dados.
Código pronto
const crypto = require('crypto'); function verifyQvardSignature(rawBody, header, secret, toleranceSec = 300) { const parts = header.split(',').map((p) => p.trim()); const t = Number((parts.find((p) => p.startsWith('t=')) || '').slice(2)); const sigs = parts.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3)); if (!t || sigs.length === 0) return false; if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest(); return sigs.some((s) => { const got = Buffer.from(s, 'hex'); return got.length === expected.length && crypto.timingSafeEqual(got, expected); }); } // Use the RAW body: express.raw, not express.json app.post('/qvard-webhook', express.raw({ type: 'application/json' }), (req, res) => { const raw = req.body.toString('utf8'); if (!verifyQvardSignature(raw, req.get('X-Qvard-Signature') || '', process.env.QVARD_SECRET_KEY)) { return res.sendStatus(400); } const { id, status } = JSON.parse(raw); // Confirm the paid amount for order `id` in your own database before fulfilling it. res.sendStatus(200); });
<?php function verifyQvardSignature(string $rawBody, string $header, string $secret, int $tolerance = 300): bool { $t = 0; $sigs = []; foreach (explode(',', $header) as $part) { $part = trim($part); if (strpos($part, 't=') === 0) { $t = (int) substr($part, 2); } elseif (strpos($part, 'v1=') === 0) { $sigs[] = substr($part, 3); } } if (!$t || !$sigs || abs(time() - $t) > $tolerance) { return false; } $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret); foreach ($sigs as $s) { if (hash_equals($expected, $s)) { return true; } } return false; } $raw = file_get_contents('php://input'); if (!verifyQvardSignature($raw, $_SERVER['HTTP_X_QVARD_SIGNATURE'] ?? '', getenv('QVARD_SECRET_KEY'))) { http_response_code(400); exit; } $event = json_decode($raw, true); // ['id' => ..., 'status' => ...] http_response_code(200); ?>
Nas 24 horas seguintes à rotação da sua chave secreta, o cabeçalho traz dois valores v1: aceite o webhook se qualquer um corresponder.
Erros comuns
- Analisar o JSON e serializá-lo novamente altera o texto. Sempre use o corpo bruto.
- Usar a chave pública (qvard_pk_…) em vez da chave secreta.
- Um relógio de servidor dessincronizado faz a verificação de 5 minutos falhar. Mantenha-o sincronizado.
- Comparar com == em vez de uma comparação em tempo constante.
Onde encontrar sua chave secreta
No painel: sua loja, Configurações, Chaves API. Guarde-a apenas no seu servidor. Se vazar, rode-a lá: a chave antiga continua funcionando por 24 horas para pagamentos (não para saques).
Chaves antigas
Se sua loja ainda tiver uma chave de formato antigo (sem o prefixo qvard_sk_), os webhooks também trazem um cabeçalho x-api-key com essa chave. Depois que você a roda, esse cabeçalho desaparece. Mude para verificar a assinatura.