Este guia leva-te de zero a um pagamento de teste funcional. Vais carregar a SDK, renderizar o botão e ligar um backend mínimo que cria a ordem e recebe o webhook.
A usar IA para programar? Salta para Construir com IA e copia um prompt pronto que faz toda esta integração por ti.

1. Configura as variáveis de ambiente

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. Carrega a SDK no frontend

<script src="https://cdn.faciconnect.com/sdks/v1/facipay.min.js"></script>
<div id="facipay-button-container"></div>

3. Renderiza o botão

O container tem de existir no DOM antes do .render().
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', 'FPSOLPG'],
      showUIOfProcessingInfo: true,
      referencePaymentLifeSpan: 1440,
    },
  },
}).render('#facipay-button-container');

4. Cria a ordem no backend

O frontend chama este endpoint dentro do createOrder(). Recalcula o total no servidor.
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,
      additionalInfo: JSON.stringify({
        returnUrl: 'https://loja/sucesso',
        cancelUrl: 'https://loja/cancelado',
        webhookUrl: 'https://loja/api/facipay/webhook',
      }),
    }),
  });

  const { data } = await fp.json();
  await db.orders.update(externalTransactionId, { referenceNumber: data.referenceNumber });
  res.json({ referenceNumber: data.referenceNumber }); // string não-vazia
});

5. Recebe o webhook

O webhook é a fonte da verdade. Valida o HMAC sobre o body cru antes de JSON.parse.
Node.js (Express)
import crypto from 'node:crypto';

// Regista 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 });
  }
);
Em desenvolvimento, expõe o teu webhook com um túnel (ngrok, Cloudflare Tunnel) e usa esse URL público no webhookUrl.

6. Testa o fluxo

1

Multicaixa Express

Clica no botão → popup → confirma. Dispara onApprove e o webhook chega com CON.
2

Referência EMIS

Escolhe Referência → o popup mostra entidade + referência. Dispara onPending.
3

Cancelar

Fecha o popup sem pagar. Dispara onCancel.

Próximo passo: Conceitos essenciais

Chaves, ambientes, ciclo de vida da ordem e idempotência.