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
}
}
| Campo | Descrição |
|---|
data.externalTransactionId | Chave de idempotência (a tua ordem). |
data.paymentStatus | PEN | CON | CAN. |
data.referenceNumber | Referência associada. |
data.amount | Valor 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.
Lê o body cru
Antes de qualquer parser JSON (ver detalhes por framework em Segurança). Compara o HMAC em tempo constante
crypto.timingSafeEqual. Tamanhos diferentes → 401.
Processa com idempotência
Estados finais (CON/CAN) não se reprocessam. Responde 200 em < 5s.
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
paymentStatus | Ação |
|---|
CON | Pago. Dispara fulfillment (confirmação, email, libertar produto). |
CAN | Cancelado. Reabre carrinho, liberta stock. |
PEN | Continua 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.