Onde vive cada segredo

CLIENT_SECRET e WEBHOOK_SECRET nunca podem aparecer no código do frontend nem no bundle enviado ao browser. Só a PUBLISHABLE_KEY é pública.

Verificação do webhook (HMAC SHA-256)

A FaciPay assina cada webhook. O header x-facipay-content-token contém o HMAC SHA-256 do body cru calculado com o teu WEBHOOK_SECRET. A ordem dos passos é crítica.
1

Lê o body CRU antes de qualquer parser JSON

Se fizeres JSON.parse primeiro, o HMAC não vai bater certo.
2

Calcula o HMAC e compara em tempo constante

Usa crypto.timingSafeEqual. Se os tamanhos diferirem, devolve 401 antes de comparar.
3

Só agora faz JSON.parse e processa

Idempotência por externalTransactionId. Responde 200 em < 5s.
Nomes de headers HTTP são case-insensitive. Em Node, req.headers['x-facipay-content-token'] funciona porque o runtime normaliza tudo para minúsculas.

Por framework

// app/api/facipay/webhook/route.ts
import crypto from 'node:crypto';

export async function POST(req: Request) {
  const raw = await req.text(); // body cru
  const token = req.headers.get('x-facipay-content-token') ?? '';
  const expected = crypto
    .createHmac('sha256', process.env.FACIPAY_WEBHOOK_SECRET!)
    .update(raw)
    .digest('hex');

  const a = Buffer.from(token);
  const b = Buffer.from(expected);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return new Response('invalid signature', { status: 401 });
  }

  const payload = JSON.parse(raw);
  // ... idempotência + atualização de estado
  return Response.json({ received: true });
}

Outras boas práticas

  • Valida a origem do popup. Ao ouvir postMessage, confirma event.origin contra o domínio oficial do checkout antes de confiar na mensagem (ver Eventos).
  • Recalcula o total no servidor. Nunca aceites o amount vindo do cliente.
  • Trata o webhook como idempotente. Reprocessar não pode duplicar fulfillment.
  • HTTPS em produção. Obrigatório para a SDK e para receber webhooks.

Próximo passo: Go-live

Checklist para passar de sandbox a produção.