# Processar eventos sem duplicar

URL: https://staging.pagpolar.com/docs/webhooks/processar-sem-duplicar

> Processe cada evento uma única vez, mesmo quando ele chega repetido, fora de ordem ou com um status posterior.

## Por que o mesmo evento chega mais de uma vez

Conte com repetições. O mesmo evento pode chegar de novo quando:

* **o seu servidor demorou mais de 10 segundos para responder.** A PagPolar conta como falha e tenta de novo, mesmo que você já tenha processado;
* **alguém reenviou os avisos pelo painel**, um por um ou em massa;
* **o gateway avisou o pagamento por mais de um caminho.** `TRANSACTION_PAID` pode ser gerado mais de uma vez para a mesma venda.

## Envios de teste

Os envios do [Playground](/docs/webhooks/playground) chegam com `"test": true` no envelope e o header `X-PagPolar-Test: true`. Os eventos reais nunca trazem esses dois. Em produção, descarte o evento de teste antes de processar.

## A ordem não é garantida

Cada evento é entregue separado, e vários são entregues ao mesmo tempo. Uma entrega que falha volta cerca de 30 segundos depois. Por isso, um `TRANSACTION_CREATED` que falhou pode chegar **depois** do `TRANSACTION_PAID` da mesma venda.

## O `id` do envelope não serve para descartar repetidos

Cada tentativa chega com um `id` novo e uma `creation_date` nova. Duas tentativas do mesmo evento têm `id` diferentes. Não use o `id` para descobrir se o evento é repetido.

## Monte a sua chave de duplicidade

Use o nome do evento e o id da venda ou da assinatura:

| Evento                                          | Chave sugerida                                                                                                                                                                     |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TRANSACTION_ASK_REFUNDING`                     | Não descarte pela chave. A mesma venda pode receber um novo pedido depois que o anterior terminou. Confira em `GET /refunds?sale_identifier=<CODIGO_DA_VENDA>` se o pedido é novo. |
| Outros eventos de venda (`TRANSACTION_*`)       | `event` + `data.transaction.id`                                                                                                                                                    |
| `SUBSCRIPTION_RENEWED`                          | `event` + `data.subscription.id` + `data.subscription.next_billing_at`                                                                                                             |
| Outros eventos de assinatura (`SUBSCRIPTION_*`) | `event` + `data.subscription.id`                                                                                                                                                   |

Cuidados com esta tabela:

* **Renovações têm id próprio.** Cada cobrança de renovação é uma venda nova, com outro `transaction.id`. A chave dos eventos de venda funciona para todos os ciclos.
* **`SUBSCRIPTION_RENEWED` chega uma vez por ciclo pago**, sempre com o mesmo `subscription.id`. Por isso a chave inclui `next_billing_at`, que muda a cada ciclo.
* **`SUBSCRIPTION_DELAYED` pode chegar uma vez por dia** enquanto a assinatura está atrasada. Com a chave sugerida, você trata só o primeiro aviso. Se quiser lembrar o cliente todo dia, não descarte esse evento.

Grave a chave numa tabela com **índice único**. Se a gravação falhar por chave repetida, o evento já foi processado.

## Responda rápido e processe depois

A resposta precisa chegar dentro do [tempo limite](/docs/webhooks/entregas-e-retentativas#sucesso). Não faça trabalho pesado antes de responder:

1. Confira o header `Authorization`.
2. Grave o evento recebido.
3. Responda `200`.
4. Processe o evento em seguida, fora da requisição.

## Não deixe o status voltar para trás

O `data` traz o [estado do momento do envio](/docs/webhooks/formato-do-evento#estado-no-envio), que pode ser mais antigo do que o que você já gravou. Antes de mudar o seu pedido:

* compare o `status` recebido com o que você já gravou;
* não volte um pedido pago para pendente por causa de um evento que chegou atrasado;
* se tiver dúvida, consulte o estado atual em [`GET /sales/{identifier}`](/docs/referencia/vendas/get-sale) ou [`GET /subscriptions/{id}`](/docs/referencia/assinaturas/get-subscription).

## O fluxo completo

Este fluxograma é uma recomendação para o seu servidor.

```mermaid
flowchart TD
  A[Evento recebido] --> B{Authorization confere?}
  B -->|Não| C[Responda 401 e descarte]
  B -->|Sim| D{data vazio?}
  D -->|Sim| E[Responda 200 e ignore]
  D -->|Não| F[Grave o evento e responda 200]
  F --> G{Chave de duplicidade já existe?}
  G -->|Sim| H[Ignore]
  G -->|Não| I{Status recebido é mais antigo que o gravado?}
  I -->|Sim| J[Consulte a API antes de mudar o pedido]
  I -->|Não| K[Aplique o efeito pelo event]
```

## Exemplo em Node.js

O exemplo usa um `Set` na memória para ficar curto. Em produção, troque o `Set` por uma tabela no banco com índice único na chave.

```js
import express from 'express';
import { timingSafeEqual } from 'node:crypto';

const app = express();
app.use(express.json());

const expectedAuthorization = Buffer.from(
  `Bearer ${process.env.PAGPOLAR_WEBHOOK_TOKEN}`,
);
const processedKeys = new Set();

const isFromPagPolar = (request) => {
  const receivedAuthorization = Buffer.from(request.get('authorization') ?? '');

  return (
    receivedAuthorization.length === expectedAuthorization.length &&
    timingSafeEqual(receivedAuthorization, expectedAuthorization)
  );
};

const buildDeduplicationKey = ({ event, data }) => {
  if (event === 'TRANSACTION_ASK_REFUNDING') {
    return null;
  }

  if (data.transaction) {
    return `${event}:${data.transaction.id}`;
  }

  if (event === 'SUBSCRIPTION_RENEWED') {
    return `${event}:${data.subscription.id}:${data.subscription.next_billing_at}`;
  }

  return `${event}:${data.subscription.id}`;
};

const handleEvent = async (payload) => {
  console.log('Processando', payload.event, payload.data.transaction?.status);
};

app.post('/webhooks/pagpolar', (request, response) => {
  if (!isFromPagPolar(request)) {
    return response.sendStatus(401);
  }

  const payload = request.body;
  response.sendStatus(200);

  if (!payload.data?.transaction && !payload.data?.subscription) {
    return;
  }

  const deduplicationKey = buildDeduplicationKey(payload);

  if (!deduplicationKey) {
    handleEvent(payload).catch((error) => {
      console.error('Falha ao processar', payload.event, error);
    });
    return;
  }

  if (processedKeys.has(deduplicationKey)) {
    return;
  }

  processedKeys.add(deduplicationKey);

  handleEvent(payload).catch((error) => {
    processedKeys.delete(deduplicationKey);
    console.error('Falha ao processar', deduplicationKey, error);
  });
});

app.listen(3000);
```

Se o processamento falhar, o exemplo apaga a chave. Assim, um reenvio do mesmo evento é processado de novo.

`TRANSACTION_ASK_REFUNDING` fica sem chave no exemplo. Dentro de `handleEvent`, confira em `GET /refunds` se o pedido é novo.

## Próximos passos

- [Catálogo de eventos](/docs/webhooks/eventos) — Veja quando cada evento é enviado.
- [Entregas e retentativas](/docs/webhooks/entregas-e-retentativas) — Reenvie avisos pelo painel.
