O webhook é a fonte da verdade do estado de um pagamento. Sempre que um pagamento muda de estado, a FaciPay envia um POST para o único endpoint registado para a sua conta.

Como os webhooks são configurados

Os webhooks são configurados ao nível da conta, não por ordem. Você fornece a URL do seu endpoint de webhook à equipa FaciPay (no onboarding ou via suporte); ela regista-a internamente contra as credenciais da sua conta. A partir daí, a FaciPay faz POST de todas as notificações de estado de pagamento da sua conta para esse único endpoint registado.
Não existe portal self-service: é a equipa FaciPay que regista a URL por si. O campo additionalInfo do createPaymentOrder é puramente informativo e não configura o webhook nem quaisquer URLs de retorno/cancelamento — a navegação de retorno/cancelamento é feita no cliente pelos callbacks da SDK (onApprove / onPending / onCancel).

Payload

{
  "event": "payment",
  "data": {
    "externalTransactionId": "order_8f2c1a9e-...",
    "paymentStatus": "CON",
    "referenceNumber": "987654321",
    "amount": 15000
  }
}
CampoDescrição
data.externalTransactionIdChave de idempotência (a sua 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 seu WEBHOOK_SECRET. Verifique-o antes de JSON.parse.
1

Leia o body cru

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

Compare o HMAC em tempo constante

crypto.timingSafeEqual. Tamanhos diferentes → 401.
3

Processe com idempotência

Estados finais (CON/CAN) não se reprocessam. Responda 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, use 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, valide sempre o HMAC como acima.