Quibly Pay
Webhooks

Verificação HMAC

Valide o corpo cru antes de processar o evento.

Validação simples: rejeite a chamada se X-Quibly-Secret não for igual ao segredo do webhook, usando comparação em tempo constante. Isso basta para a maioria das integrações que recebem esse header. Em outra origem, onde ele não é enviado, valide a assinatura HMAC.

Assinatura opcional: HMAC-SHA256 hexadecimal usando toda a chave de assinatura literal (whsig_... ou o valor legado whsec_...) como chave UTF-8, sobre timestamp + "." + os bytes exatos do corpo JSON cru. Não remova o prefixo, não decodifique a chave de base64 nem serialize o JSON novamente. Capture o corpo cru antes de qualquer interpretador JSON. Compare as assinaturas em tempo constante. Os exemplos abaixo recomendam uma janela de validade de 300 segundos contra reenvios maliciosos; essa janela é uma política do receptor, não uma configuração da API. Cada retentativa é assinada com seu horário atual.

Comparação simples do segredo (Node.js)

import { timingSafeEqual } from 'node:crypto';
export function hasQuiblySecret(headerValue, secret) {
  const a = Buffer.from(headerValue ?? '');
  const b = Buffer.from(secret);
  return a.length === b.length && timingSafeEqual(a, b);
}
// secret = process.env.QP_WEBHOOK_SECRET; headerValue = o header x-quibly-secret da chamada.
// Rejeite com 401 antes de processar o evento se o resultado for falso.

Verificação no receptor Node.js

import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyQuibly(rawBody, timestamp, signature, secret) {
  // rawBody deve ser o Buffer original, sem usar JSON.stringify(parsedBody).
  if (!/^\d+$/.test(timestamp ?? '') || !/^sha256=[a-f0-9]{64}$/.test(signature ?? ''))
    return false;
  const seconds = Number(timestamp);
  if (!Number.isSafeInteger(seconds) || Math.abs(Date.now() / 1000 - seconds) > 300) return false;
  const expected = createHmac('sha256', secret)
    .update(timestamp + '.')
    .update(rawBody)
    .digest();
  const received = Buffer.from(signature.slice(7), 'hex');
  return received.length === expected.length && timingSafeEqual(expected, received);
}
// secret = a chave de assinatura em process.env.QP_WEBHOOK_SIGNING_KEY.
// Extraia x-quibly-timestamp e x-quibly-signature da chamada recebida.
// Rejeite com 401 antes de interpretar ou processar o JSON se verifyQuibly retornar falso.

On this page