PayZuDocs
Boas práticas

Idempotência

Chamar a mesma operação de novo não deveria cobrar duas vezes nem dar baixa em dobro: veja como usar a sua referência para amarrar isso e por que os callbacks chegam repetidos.

Idempotência é a garantia de que chamar a mesma operação várias vezes tem o mesmo efeito que chamar uma vez só. Sem isso, retries viram cobranças duplicadas, baixas em dobro e estornos perdidos.

Cenários que exigem idempotência

CenárioSem idempotênciaCom idempotência
Sua app crasha após POST /pix, mas não sabe se chegouGera 2 cobrançasPayZu devolve a existente
POST /pix deu timeout, mas o QR foi geradoCliente vê 2 QRs diferentesPayZu devolve a mesma transação
Job de retry dispara a mesma cobrança 2x2 cobranças, suporte ruim1 cobrança, cliente paga normalmente
Mesmo callback chega 2 vezes (retry após timeout)Marca pedido pago 2xIgnora o duplicado
Transação passa por PENDING → COMPLETED → REFUNDEDPode ignorar o estornoProcessa cada transição uma única vez

clientReference na criação

clientReference é o identificador externo idempotente que você define ao criar uma cobrança, saque ou transferência. A PayZu indexa por userId + clientReference e devolve a transação existente se ela já foi criada.

Como gerar

PadrãoQuando usar
order-{orderId}1 cobrança por pedido. Recomendado.
payout-{payoutId}1 saque por solicitação.
subscription-{subId}-{period}Cobranças recorrentes (1 por ciclo).
retry-{orderId}-{attempt}Quando você precisa forçar uma nova cobrança após falha definitiva.
transfer-{from}-{to}-{date}Transferências internas idempotentes por dia.

Nunca use Date.now(), uuid() ou outro valor aleatório como clientReference. O retry vai gerar valor diferente e a PayZu vai criar cobrança duplicada, quebrando exatamente a garantia que você queria ter.

curl -X POST https://api.payzu.processamento.com/v1/pix \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 99.90,
    "clientReference": "order-1234",
    "callbackUrl": "https://seusite.com.br/webhooks/payzu"
  }'

O mesmo request em outras linguagens está no tutorial Receber pagamento Pix.

Dedupe de callbacks por id + status

O mesmo callback pode chegar mais de uma vez:

A chave de dedupe deve ser id + status, não só id. Se você usar só id, vai ignorar o callback de REFUNDED porque já viu COMPLETED antes, e estorno não dá baixa.

Implementação

import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
const TTL_30_DIAS = 30 * 86400;

type PayzuCallback = {
  id: string;
  type: 'DEPOSIT' | 'WITHDRAW';
  method: 'PIX' | 'BANK_SLIP' | 'INTERNAL_TRANSFER';
  status: 'PENDING' | 'COMPLETED' | 'CANCELED' | 'WAITING_FOR_REFUND' | 'REFUNDED' | 'EXPIRED' | 'ERROR';
  clientReference?: string;
};

async function handleCallback(tx: PayzuCallback) {
  const dedupeKey = `payzu:${tx.id}:${tx.status}`;
  const isFirstTime = await redis.set(dedupeKey, '1', 'EX', TTL_30_DIAS, 'NX');
  if (!isFirstTime) return;

  await processTransaction(tx);
}

Armadilhas comuns

ArmadilhaSintoma
clientReference aleatório a cada retryCobrança duplicada, cliente confuso
Dedupe usando só id (sem status)Estorno não dá baixa, refund "fantasma"
TTL do dedupe muito curtoRetry tardio recria processamento
Dedupe em memória (Map local)Após restart, processa tudo de novo
Recriar clientReference com Date.now() por achar que "muda"Não dispara idempotência, gera nova cobrança

Nesta página