O webhook é a fonte da verdade do estado de um pagamento. A FaciPay envia um POST para o webhookUrl (indicado em additionalInfo ao criar a ordem) sempre que o estado muda.

Payload

{
  "event": "payment",
  "data": {
    "externalTransactionId": "order_8f2c1a9e-...",
    "paymentStatus": "CON",
    "referenceNumber": "987654321",
    "amount": 15000
  }
}
CampoDescrição
data.externalTransactionIdChave de idempotência (a tua ordem).
data.paymentStatusPEN | CON | CAN.
data.referenceNumberReferência associada.
data.amountValor em AOA (inteiro).

Verificação da assinatura

Cada webhook é assinado. O header x-facipay-content-token contém o HMAC SHA-256 do body cru calculado com o teu WEBHOOK_SECRET. Verifica-o antes de JSON.parse.
1

Lê o body cru

Antes de qualquer parser JSON (ver detalhes por framework em Segurança).
2

Compara o HMAC em tempo constante

crypto.timingSafeEqual. Tamanhos diferentes → 401.
3

Processa com idempotência

Estados finais (CON/CAN) não se reprocessam. Responde 200 em < 5s.
Node.js (Express)
import crypto from 'node:crypto';

app.post('/api/facipay/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const raw = req.body;
    const token = String(req.headers['x-facipay-content-token'] ?? '');
    const expected = crypto
      .createHmac('sha256', process.env.FACIPAY_WEBHOOK_SECRET)
      .update(raw).digest('hex');

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

    const { data } = JSON.parse(raw.toString('utf8'));
    updateOrder(data.externalTransactionId, data.paymentStatus); // idempotente
    res.status(200).json({ received: true });
  });

Mapeamento de estados

paymentStatusAção
CONPago. Dispara fulfillment (confirmação, email, libertar produto).
CANCancelado. Reabre carrinho, liberta stock.
PENContinua pendente.

Idempotência

function updateOrder(externalTransactionId, paymentStatus) {
  const order = db.orders.find(externalTransactionId);
  if (!order) return;
  if (order.status === 'CON' || order.status === 'CAN') return; // já final
  db.orders.update(externalTransactionId, { status: paymentStatus });
}
Tarefas pesadas (emails, ERP, faturação) vão para fila/background. O handler do webhook deve responder 200 em menos de 5 segundos.

Fallback

Se o webhook falhar ou atrasar, usa GET /facipaypartner/paymentByExternalTransaction para consultar o estado. É apenas rede de segurança — o webhook continua a ser a fonte da verdade.
A integração de referência (fake-store) recebe o webhook mas não valida a assinatura (é didática). Em produção, valida sempre o HMAC como acima.