---
agentTools:
  projectIndex: https://docs.ninepay.com.br/llms.txt
  projectSearch: https://docs.ninepay.com.br/agentSearch?query=term1%20term2
---

# Receber e validar webhooks

Receba os eventos da NinePay, responda rápido, valide a assinatura, ignore entregas repetidas e confirme o pagamento na API antes de liberar o acesso.

## Exponha um endereço público

> Linhas destacadas: 8-11, 41

Cadastre a URL do seu servidor em **Integrações → Webhooks** no dashboard ou com `POST /automations` (veja [Webhooks](/docs/webhooks)). O endereço precisa ser público; use `https`. O formulário do webhook tem um envio de teste para cada evento.

## Leia o corpo cru e o evento

> Linhas destacadas: 12-14, 31

Cada entrega é um `POST` com JSON: `id` (o da entrega), `type` (o evento, como `payment.approved` ou `subscription.updated`), `data` (o recurso no mesmo formato da API) e `created_at`. Leia o corpo como bytes, sem fazer parse antes de validar: a assinatura é calculada sobre o corpo exatamente como chegou.

## Valide a assinatura

> Linhas destacadas: 16-29

Cada entrega de um webhook com segredo traz `X-Ninepay-Timestamp` e `X-Ninepay-Signature`. Calcule `sha256=` + HMAC-SHA256 de `"{timestamp}.{corpo cru}"`, com o segredo do webhook (`nep_whs_…`) como chave, e compare com o header em tempo constante. Recuse com 401 se não bater ou se o timestamp tiver mais de 5 minutos de diferença para o seu relógio. A assinatura muda a cada tentativa, então valide todas.

O segredo aparece uma vez só, ao criar o webhook ou ao clicar em **Gerar novo segredo**. Webhooks criados antes da assinatura não têm segredo, e não vêm assinados, até você clicar em **Gerar segredo** em **Integrações → Webhooks**.

## Responda em até 10 segundos

> Linhas destacadas: 33-34

Qualquer status fora de 2xx, ou a falta de resposta, conta como falha, e a entrega é repetida até 10 vezes, com intervalos crescentes. Responda primeiro e deixe o processamento pesado para depois (numa fila, por exemplo).

## Ignore entregas repetidas

> Linhas destacadas: 36-37

O `id` é o mesmo em todas as tentativas de uma entrega. Guarde os ids já processados para não liberar nada duas vezes. Os eventos também podem chegar fora de ordem.

## Confirme na API antes de liberar

> Linhas destacadas: 39-51

A assinatura prova que foi a NinePay quem enviou, mas não substitui a consulta. Trate o evento como um aviso: consulte `GET /payments/{id}` com o seu token e libere só se o `status` for `paid`. Assim, um `POST` forjado ou repetido para o seu endereço não libera nada.

## Código

```js Node.js
import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';

const BASE_URL = 'https://api.sandbox.cefispay.com.br/api/v1';
const processed = new Set(); // em produção, guarde no seu banco

createServer(async (req, res) => {
  if (req.method !== 'POST' || req.url !== '/webhooks/ninepay') {
    res.writeHead(404).end();
    return;
  }
  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);
  const rawBody = Buffer.concat(chunks); // corpo cru, antes de qualquer parse

  // Valide a assinatura antes de confiar no evento
  const timestamp = req.headers['x-ninepay-timestamp'] ?? '';
  const signature = req.headers['x-ninepay-signature'] ?? '';
  const expected = 'sha256=' + createHmac('sha256', process.env.NINEPAY_WEBHOOK_SECRET)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest('hex');
  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  const fresh = /^\d+$/.test(timestamp) && Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
  if (!fresh || a.length !== b.length || !timingSafeEqual(a, b)) {
    res.writeHead(401).end();
    return;
  }

  const event = JSON.parse(rawBody.toString('utf8'));

  // Responda 2xx logo (em até 10 s) e processe depois
  res.writeHead(200).end();

  if (processed.has(event.id)) return; // a mesma entrega pode chegar de novo
  processed.add(event.id);

  if (event.type === 'payment.approved') {
    // Mesmo com a assinatura válida, confirme o estado na API
    const apiRes = await fetch(`${BASE_URL}/payments/${event.data.id}`, {
      headers: {
        Authorization: `Bearer ${process.env.NINEPAY_TOKEN}`,
        Accept: 'application/json',
      },
    });
    const { data: payment } = await apiRes.json();
    if (payment.status === 'paid') {
      // libere o acesso do cliente aqui
    }
  }
}).listen(3000);
```
