# Segurança Proteja segredos e valide o webhook do FaciPay com HMAC SHA-256 — passo a passo por framework. ## Onde vive cada segredo ```mermaid flowchart LR subgraph Frontend [Frontend - público] PK[PUBLISHABLE_KEY] end subgraph Backend [Backend - privado] CID[CLIENT_ID] CS[CLIENT_SECRET] WS[WEBHOOK_SECRET] end ``` `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 seu `WEBHOOK_SECRET`. A ordem dos passos é crítica. Se fizer `JSON.parse` primeiro, o HMAC não vai bater certo. Use `crypto.timingSafeEqual`. Se os tamanhos diferirem, devolva `401` antes de comparar. Localize a ordem por `externalTransactionId` e deduplique por `paymentId` + `paymentStatus`. Responda `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 ```ts Next.js (App Router) // 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 }); } ``` ```ts Express import crypto from 'node:crypto'; // raw parser SÓ nesta rota (não global) app.post('/api/facipay/webhook', express.raw({ type: 'application/json' }), (req, res) => { const raw = req.body; // Buffer 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 payload = JSON.parse(raw.toString('utf8')); // ... processa res.status(200).json({ received: true }); }); ``` ```ts NestJS // main.ts const app = await NestFactory.create(AppModule, { rawBody: true }); // controller @Post('webhook') handle(@Req() req: RawBodyRequest, @Headers('x-facipay-content-token') token: string) { const raw = req.rawBody!; // Buffer 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)) { throw new UnauthorizedException(); } const payload = JSON.parse(raw.toString('utf8')); // ... } ``` ## Outras boas práticas - **Valide a origem do popup.** Ao ouvir `postMessage`, confirme `event.origin` contra o domínio oficial do checkout antes de confiar na mensagem (ver [Eventos](/pt/sdk/events)). - **Recalcule o total no servidor.** Nunca aceite o `amount` vindo do cliente. - **Trate o webhook como idempotente.** Reprocessar não pode duplicar fulfillment. - **HTTPS em produção.** Obrigatório para a SDK e para receber webhooks. Checklist para passar de sandbox a produção.