# 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.