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

  1. 1
    Pegue o corpo da solicitação exatamente como chegou: o texto bruto, antes de qualquer análise JSON.
  2. 2
    Leia t e cada v1 do cabeçalho X-Qvard-Signature.
  3. 3
    Calcule HMAC-SHA256 do texto t + "." + corpo com sua chave secreta (qvard_sk_…) e escreva em hexadecimal minúsculo.
  4. 4
    Se seu resultado corresponder a qualquer v1 (compare em tempo constante), a mensagem é genuína.
  5. 5
    Verifique se t não é mais velho do que 5 minutos, para que uma mensagem antiga não possa ser repetida.
  6. 6
    Apenas 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);
});

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

Abrir painel

Referência da API

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.

Quer experimentar a janela de pagamento?

Abrir demonstração