Processar eventos 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_PAIDpode ser gerado mais de uma vez para a mesma venda.
Envios de teste
Os envios do 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_RENEWEDchega uma vez por ciclo pago, sempre com o mesmosubscription.id. Por isso a chave incluinext_billing_at, que muda a cada ciclo.SUBSCRIPTION_DELAYEDpode 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. Não faça trabalho pesado antes de responder:
- Confira o header
Authorization. - Grave o evento recebido.
- Responda
200. - 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, que pode ser mais antigo do que o que você já gravou. Antes de mudar o seu pedido:
- compare o
statusrecebido 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}ouGET /subscriptions/{id}.
O fluxo completo
Este fluxograma é uma recomendação para o seu servidor.
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.
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.