PT

Webhooks de pagamentos cripto: verifique a assinatura HMAC, trate novas tentativas e entregas duplicadas

Webhooks da SnapEX Pay

O webhook é a forma de a sua loja, o seu sistema de faturamento ou o seu bot saber que o cliente pagou. A SnapEX Pay envia um HTTP POST para a sua URL a cada evento de fatura, pagamento e saque. Bem implementado, ele é o sinal mais rápido e confiável que você tem. Feito com descuido, vira um jeito de marcar como pagos pedidos que nunca foram pagos. Este guia mostra o que importa.

Configure o webhook

No painel do comerciante, abra a página “Desenvolvedores” → bloco “Webhook”, informe a sua URL e clique em “Salvar”. Cada conta tem uma única URL, que recebe todos os tipos de evento. Ao salvar pela primeira vez, o painel mostra uma única vez a chave secreta whsec_…, usada na assinatura HMAC: guarde-a apenas no seu servidor.

A URL precisa ser um endereço https:// absoluto, com host público: sem login:password, sem localhost, e o host não pode resolver para um IP privado ou reservado. Redirecionamentos não são seguidos, então informe a URL final.

O que você recebe

Toda requisição traz quatro headers:

Header Significado
Webhook-Id ID único do evento. Não muda nas novas tentativas nem nos reenvios manuais.
Webhook-Timestamp Tempo Unix (em segundos) em que esta tentativa foi enviada. Cada nova tentativa tem um valor novo.
Webhook-Signature v1= e o HMAC-SHA256 em hex minúsculo de {Webhook-Timestamp}.{raw body}.
Content-Type Sempre application/json.

O corpo tem id (igual ao Webhook-Id), type, created_at e data. Nos eventos invoice.*, data é a fatura no mesmo formato de GET /invoices/{id}, incluindo environment, amount_usd, status e o seu external_id.

Os eventos de fatura:

Evento Quando
invoice.created Fatura criada, ainda sem forma de pagamento escolhida.
invoice.awaiting A forma de pagamento foi escolhida e o endereço foi gerado.
invoice.underpaid Chegou parte do valor; o mesmo endereço aguarda o restante.
invoice.paid O valor total foi pago e creditado. O único sinal para liberar o pedido.
invoice.partially_paid Só parte do valor foi paga, o prazo para completar o pagamento terminou e a parte recebida foi creditada a você.
invoice.expired O prazo da fatura terminou sem pagamento.
invoice.extra_payment Chegou outra transferência depois que a fatura já estava paga; ela é creditada à parte.

Verifique a assinatura HMAC

  1. Leia o corpo bruto da requisição como bytes, antes de qualquer parsing de JSON. Um JSON serializado de novo não vai bater.
  2. Calcule o HMAC-SHA256 de {Webhook-Timestamp}.{raw body} usando como chave a chave secreta inteira, incluindo o prefixo whsec_, e codifique o resultado em hex.
  3. Compare v1= + hex com o header Webhook-Signature usando uma função de comparação em tempo constante.
  4. Rejeite o Webhook-Timestamp que diferir do seu relógio em mais de 5 minutos. As novas tentativas trazem um timestamp novo, então essa regra não as atrapalha.

PHP:

$secret = getenv('SNAPEX_WEBHOOK_SECRET');          // whsec_… from the cabinet
$body   = file_get_contents('php://input');           // raw body, before json_decode
$ts     = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$sig    = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';

$expected = 'v1=' . hash_hmac('sha256', $ts . '.' . $body, $secret);
if (!hash_equals($expected, $sig) || abs(time() - (int) $ts) > 300) {
    http_response_code(400);
    exit;
}

$event = json_decode($body, true);
// skip if $event['id'] was already processed; do the heavy work in a queue
http_response_code(200);

Node.js (Express):

const crypto = require('crypto');

app.post('/hooks/snapex', express.raw({ type: 'application/json' }), (req, res) => {
  const ts = req.get('Webhook-Timestamp') || '';
  const sig = Buffer.from(req.get('Webhook-Signature') || '');
  const expected = Buffer.from('v1=' + crypto
    .createHmac('sha256', process.env.SNAPEX_WEBHOOK_SECRET)
    .update(ts + '.' + req.body)
    .digest('hex'));

  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
  if (!fresh || sig.length !== expected.length || !crypto.timingSafeEqual(sig, expected)) {
    return res.sendStatus(400);
  }

  const event = JSON.parse(req.body);
  // skip if event.id was already processed; queue the heavy work
  res.sendStatus(200);
});

Novas tentativas, entregas duplicadas e ordem

  • Sucesso é qualquer 2xx. Qualquer outro código, timeout ou erro de rede conta como falha. Os timeouts são de 5 segundos para conectar e de 15 segundos para a requisição inteira, então responda rápido e faça o trabalho pesado de forma assíncrona.
  • Novas tentativas. Até 5 tentativas por evento: a primeira e, depois de cada falha, novas tentativas após cerca de 10 segundos, 1 minuto, 5 minutos e 15 minutos. Depois da quinta falha, a entrega automática para; o painel mostra as entregas recentes no bloco “Entregas do webhook” e tem o botão “Reenviar”, que envia o evento de novo com o mesmo Webhook-Id.
  • Entregas duplicadas. A entrega é do tipo at-least-once (pelo menos uma vez): o mesmo evento pode chegar mais de uma vez. Guarde os valores de Webhook-Id já processados e ignore as repetições.
  • Ordem. Os eventos podem chegar fora de ordem: um evento antigo, enviado em nova tentativa, pode chegar depois de um mais recente. Não dependa da ordem; compare o status que vem no evento com o que você já tem.
  • Confira de novo, se quiser. Depois de um webhook, você pode consultar o objeto outra vez pela API: GET /invoices/{id}.

Teste sem dinheiro real

Crie uma fatura de teste ("environment": "test"), pague-a na página de pagamento e acompanhe a chegada dos webhooks com data.environment = "test". Detalhes: Como testar pagamentos em cripto sem dinheiro real.

Os módulos prontos para WHMCS, BILLmanager, WooCommerce e outras plataformas já verificam a assinatura HMAC e nunca creditam um pagamento duas vezes; veja pay.snapex.pro. Referência completa: documentação da API da SnapEX Pay.

Pronto para aceitar cripto?

Crie uma conta de comerciante e obtenha uma chave de API. 1% por pagamento, sem mensalidade.