# 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.