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.
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.
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
Multicaixa Express
Clica no botão → popup → confirma. Dispara onApprove e o webhook chega com CON.
Referência EMIS
Escolhe Referência → o popup mostra entidade + referência. Dispara onPending.
Cancelar
Fecha o popup sem pagar. Dispara onCancel.
Próximo passo: Conceitos essenciais Chaves, ambientes, ciclo de vida da ordem e idempotência.