# Webhooks
Receber, verificar e processar notificações de estado de pagamento.
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
```json
{
"event": "payment",
"data": {
"externalTransactionId": "order_8f2c1a9e-...",
"paymentStatus": "CON",
"referenceNumber": "987654321",
"amount": 15000
}
}
```
| Campo | Descrição |
|---|---|
| `data.externalTransactionId` | Chave de idempotência (a sua 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 seu `WEBHOOK_SECRET`. Verifique-o **antes** de `JSON.parse`.
Antes de qualquer parser JSON (ver detalhes por framework em [Segurança](/pt/get-started/security)).
`crypto.timingSafeEqual`. Tamanhos diferentes → `401`.
Estados finais (`CON`/`CAN`) não se reprocessam. Responda `200` em < 5s.
```js 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
| `paymentStatus` | Ação |
|---|---|
| `CON` | Pago. Dispara fulfillment (confirmação, email, libertar produto). |
| `CAN` | Cancelado. Reabre carrinho, liberta stock. |
| `PEN` | Continua pendente. |
## Idempotência
```js
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`](/api-reference/payment-orders/consultar-pagamento-por-externaltransactionid) 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.