# Início rápido
Aceite o seu primeiro pagamento FaciPay em sandbox em ~10 minutos: SDK, botão, backend e webhook.
Este guia leva você de zero a um pagamento de teste funcional. Vai carregar a SDK,
renderizar o botão e ligar um backend mínimo que cria a ordem e recebe o webhook.
**Como obter as credenciais.** As chaves (`pk_…`, `clientId`, `clientSecret`, `webhookSecret`)
são emitidas pela FaciPay após abrir uma **conta de negócio** e assinar contrato com a
área comercial da **Faci-X**. Para começar, fale connosco: 📞 +244 923 168 670 · ✉️ [atendimento@faci-x.ao](mailto:atendimento@faci-x.ao).
A usar IA para programar? Salte para [Construir com IA](/pt/get-started/build-with-ai) e copie
um prompt pronto que faz toda esta integração por si.
## 1. Configure as variáveis de ambiente
```bash .env
# Apenas backend — nunca exponha no bundle do frontend
FACIPAY_CLIENT_ID=xxxxxxxx
FACIPAY_CLIENT_SECRET=xxxxxxxx
FACIPAY_WEBHOOK_SECRET=xxxxxxxx
# Seguro expor no frontend
FACIPAY_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxx
# URL base: sandbox vs produção
FACIPAY_API_URL=https://sandbox.api.faciconnect.com
```
`CLIENT_SECRET` e `WEBHOOK_SECRET` ficam **só no backend**. Apenas a `PUBLISHABLE_KEY`
pode ir para o frontend. O `applicationUUID` usado na API é a `PUBLISHABLE_KEY` **sem**
o prefixo `pk_test_`/`pk_live_`.
## 2. Carregue a SDK no frontend
```html
```
## 3. Renderize o botão
O container **tem** de existir no DOM antes do `.render()`.
```js
const facipay = FaciPay(PUBLISHABLE_KEY);
facipay.generateButton({
async createOrder() {
const r = await fetch('/api/facipay/create-order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: cart }),
});
if (!r.ok) throw new Error('Falha ao criar ordem');
const { referenceNumber } = await r.json();
return referenceNumber; // string não-vazia, obrigatório
},
async onApprove(data, actions) {
actions.onPopupWindowClosed(() => {
window.location.href = `/sucesso?orderId=${data.payment.orderId}`;
});
},
async onPending(data, actions) {
actions.onPopupWindowClosed(() => {
const ref = data.payment?.data?.paymentReference;
const entity = data.payment?.data?.entity?.number;
window.location.href = `/pendente?ref=${ref}&entity=${entity}`;
});
},
async onCancel() { window.location.href = '/cancelado'; },
async onError(e) { console.error('FaciPay error:', e); },
options: {
style: { width: '100%', shape: 'pill' },
config: { lang: 'pt', showAmount: true },
paymentConfig: {
theme: 'light',
allowedPaymentMethods: ['FPMCXEXPRSS', 'FPSOLPGEXT', 'FPSOLPG'],
showUIOfProcessingInfo: true,
referencePaymentLifeSpan: 1440,
},
},
}).render('#facipay-button-container');
```
## 4. Crie a ordem no backend
O frontend chama este endpoint dentro do `createOrder()`. **Recalcule o total no servidor.**
```js Node.js (Express)
import express from 'express';
let cachedToken = null; // cache em memória até ~60s antes de expirar
async function getAccessToken() {
if (cachedToken && cachedToken.exp > Date.now()) return cachedToken.value;
const basic = Buffer.from(
`${process.env.FACIPAY_CLIENT_ID}:${process.env.FACIPAY_CLIENT_SECRET}`
).toString('base64');
const res = await fetch(`${process.env.FACIPAY_API_URL}/token`, {
method: 'POST',
headers: {
Authorization: `Basic ${basic}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'grant_type=client_credentials&validity_period=3600',
});
const json = await res.json();
cachedToken = { value: json.access_token, exp: Date.now() + 3540 * 1000 };
return cachedToken.value;
}
app.post('/api/facipay/create-order', express.json(), async (req, res) => {
const amount = recalcTotalFromItems(req.body.items); // inteiro AOA, no servidor
const externalTransactionId = 'order_' + crypto.randomUUID();
await db.orders.insert({ externalTransactionId, amount, status: 'PEN' });
const token = await getAccessToken();
const applicationUUID = process.env.FACIPAY_PUBLISHABLE_KEY.replace(/^pk_(test|live)_/, '');
const fp = await fetch(`${process.env.FACIPAY_API_URL}/facipaypartner/createPaymentOrder`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Accept-Language': 'pt',
'Content-Type': 'application/json',
},
body: JSON.stringify({
externalTransactionId,
clientId: req.user?.id ?? 'guest',
applicationUUID,
name: 'Compra',
amount,
quantity: 1,
}),
});
const { data } = await fp.json();
await db.orders.update(externalTransactionId, { referenceNumber: data.referenceNumber });
res.json({ referenceNumber: data.referenceNumber }); // string não-vazia
});
```
## 5. Receba o webhook
O webhook é a **fonte da verdade**. Valide o HMAC sobre o **body cru** antes de `JSON.parse`.
```js Node.js (Express)
import crypto from 'node:crypto';
// Registe o parser raw SÓ nesta rota
app.post('/api/facipay/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const raw = req.body; // Buffer
const token = req.headers['x-facipay-content-token'];
const expected = crypto
.createHmac('sha256', process.env.FACIPAY_WEBHOOK_SECRET)
.update(raw)
.digest('hex');
const a = Buffer.from(String(token));
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).end(); // assinatura inválida
}
const payload = JSON.parse(raw.toString('utf8'));
const { externalTransactionId, paymentStatus } = payload.data;
// Idempotência: se já está em estado final, responde 200 e sai
// Atualiza a ordem: CON = pago, CAN = cancelado, PEN = pendente
updateOrder(externalTransactionId, paymentStatus);
res.status(200).json({ received: true });
}
);
```
O webhook é **global / ao nível da conta**: forneça o seu URL de webhook público à equipa
FaciPay para que o registe contra as suas credenciais (no onboarding ou via suporte). Não há
campo `webhookUrl` na ordem — o mesmo endpoint recebe **todas** as suas notificações.
Em desenvolvimento, exponha o seu webhook com um túnel (ngrok, Cloudflare Tunnel) e dê esse
URL público à FaciPay para o registar.
## 6. Teste o fluxo
Clique no botão → popup → confirme. Dispara `onApprove` e o webhook chega com `CON`.
Escolha Referência → o popup mostra entidade + referência. Dispara `onPending`.
Feche o popup sem pagar. Dispara `onCancel`.
Chaves, ambientes, ciclo de vida da ordem e idempotência.