Pular para o conteúdo principal

Princípios

Toda implementação de webhook precisa cobrir 3 coisas:
  1. Validação HMAC com o secret recebido na criação do webhook
  2. Resposta rápida (200 OK em ≤10s)
  3. Idempotência via X-NTXPay-Delivery ou transaction.id

Exemplos de Código

import express from 'express';
import crypto from 'crypto';

const app = express();

// CRITICAL: use raw body, not parsed JSON, so HMAC matches
app.use('/webhooks/ntxpay', express.raw({ type: 'application/json' }));

const SECRET = process.env.NTXPAY_WEBHOOK_SECRET!;
const seen = new Set<string>(); // production: Redis with TTL

app.post('/webhooks/ntxpay', async (req, res) => {
  const sig = req.header('X-NTXPay-Signature') ?? '';
  const deliveryId = req.header('X-NTXPay-Delivery') ?? '';

  const expected = 'sha256=' + crypto
    .createHmac('sha256', SECRET)
    .update(req.body)
    .digest('hex');

  if (sig.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).end();
  }

  // Dedupe
  if (seen.has(deliveryId)) return res.json({ duplicate: true });
  seen.add(deliveryId);

  const event = JSON.parse(req.body.toString());

  // Process async — don't block response
  enqueue(event).catch(console.error);

  res.json({ received: true });
});

Por que raw body?

O HMAC é calculado sobre os bytes exatos que o NTX Pay enviou. Se o framework parsear o JSON antes (rearranjando espaços, reordenando campos), a assinatura não bate. Sempre capture o body bruto em bytes antes de fazer parse.

Eventos por Status

Filtre antes de processar:
const event = JSON.parse(req.body.toString());

switch (event.event) {
  case 'cash_in':
    if (event.transaction.status === 'CONFIRMED') {
      await markOrderPaid(event.transaction.externalId);
    }
    break;

  case 'cash_out':
    if (event.transaction.status === 'CONFIRMED') {
      await markPayoutSettled(event.transaction.id);
    } else if (event.transaction.status === 'FAILED') {
      await markPayoutFailed(event.transaction.id);
    }
    break;

  case 'refund_in':
  case 'refund_out':
    await processRefund(event);
    break;
}

Re-Tentativas

Se você devolver status ≠ 2xx, o NTX Pay retenta até 5 vezes em backoff exponencial (~30s, 1m, 5m, 15m, 1h). Depois disso, o evento é descartado. Para reenviar manualmente, use o painel ou contate suporte.
Não use 429 para sinalizar rate-limit do seu próprio serviço — isso aciona retry e amplifica a carga. Responda 503 Service Unavailable se realmente não puder processar.

Boas Práticas

  • Use Redis/banco para dedupe com TTL ≥ 24h (não memória in-process)
  • Processe assíncrono: webhook handler só valida + enfileira
  • Monitore latência do handler — alvo P95 < 500ms
  • Logue X-NTXPay-Delivery para auditoria
  • Re-consulte GET /api/spei/transaction/{externalId} se o webhook trouxer estado conflitante com seu banco