Pular para o conteúdo principal

Quando dispara

O evento cash_out é disparado em dois cenários:
  • Sucesso — o SPEI cash-out enviado via POST /api/spei/cash-out foi liquidado no Banxico (status: CONFIRMED)
  • Falha — a rede SPEI ou o provider rejeitou a transferência (status: FAILED)

Payload (confirmado)

{
  "event": "cash_out",
  "deliveryId": "1a3f9e8d-2c47-4b9c-aa18-77a2b3c4d5e6",
  "createdAt": "2026-05-13T12:00:42.000Z",
  "transaction": {
    "id": 56789,
    "externalId": "payout-001",
    "paymentMethod": "SPEI",
    "direction": "out",
    "type": "cash_out",
    "status": "CONFIRMED",
    "provider": "smartfastpay",
    "amountCentavos": 50000,
    "clabe": "012180001234567890",
    "createdAt": "2026-05-13T12:00:00.000Z",
    "confirmedAt": "2026-05-13T12:00:42.000Z"
  }
}

Payload (falha)

{
  "event": "cash_out",
  "deliveryId": "...",
  "createdAt": "2026-05-13T12:01:00.000Z",
  "transaction": {
    "id": 56789,
    "status": "FAILED",
    "paymentMethod": "SPEI",
    "direction": "out",
    "type": "cash_out",
    "amountCentavos": 50000,
    "clabe": "012180001234567890",
    "createdAt": "2026-05-13T12:00:00.000Z",
    "confirmedAt": null
  }
}
Quando status: FAILED, o saldo bloqueado é devolvido automaticamente.

Headers

HeaderValor
X-NTXPay-Eventcash_out
X-NTXPay-Signaturesha256=<hmac>
X-NTXPay-TimestampUnix epoch
X-NTXPay-DeliveryUUID

Comportamento

  • At-least-once: você pode receber CONFIRMED mais de uma vez. Deduplique por transaction.id.
  • Falha após sucesso: não acontece. Uma transação não muda de CONFIRMED para FAILED.
  • Reversão: se a contraparte (beneficiário) devolver, você recebe um evento refund_in separado, com transaction.type = "refund_in" linkado pelo externalId.

Resposta Esperada

HTTP/1.1 200 OK

Exemplos de Handler

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

const app = express();
app.use(express.raw({ type: 'application/json' })); // raw body for HMAC

const SECRET = process.env.NTXPAY_WEBHOOK_SECRET!;

app.post('/webhooks/ntxpay', (req, res) => {
  const sig = req.header('X-NTXPay-Signature') ?? '';
  const expected = 'sha256=' + crypto
    .createHmac('sha256', SECRET)
    .update(req.body) // req.body is Buffer
    .digest('hex');

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

  const event = JSON.parse(req.body.toString());
  if (event.event === 'cash_out') {
    if (event.transaction.status === 'CONFIRMED') {
      markPayoutSettled(event.transaction.id);
    } else if (event.transaction.status === 'FAILED') {
      markPayoutFailed(event.transaction.id);
    }
  }

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