# Idempotência (/docs/pix-processamento/best-practices/idempotency)



<QuickLinks>
  <QuickLink href="/docs/pix-processamento/endpoints/pix-operations/post_pix" title="POST /pix" />

  <QuickLink href="/docs/pix-processamento/endpoints/pix-operations/get_pix" title="GET /pix" />

  <QuickLink href="/docs/pix-processamento/webhooks" title="Webhooks" />
</QuickLinks>

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ários-que-exigem-idempotência]

| Cenário                                                 | Sem idempotência            | Com idempotência                      |
| ------------------------------------------------------- | --------------------------- | ------------------------------------- |
| Sua app crasha após `POST /pix`, mas não sabe se chegou | Gera 2 cobranças            | PayZu devolve a existente             |
| `POST /pix` deu timeout, mas o QR foi gerado            | Cliente vê 2 QRs diferentes | PayZu devolve a mesma transação       |
| Job de retry dispara a mesma cobrança 2x                | 2 cobranças, suporte ruim   | 1 cobrança, cliente paga normalmente  |
| Mesmo callback chega 2 vezes (retry após timeout)       | Marca pedido pago 2x        | Ignora o duplicado                    |
| Transação passa por `PENDING → COMPLETED → REFUNDED`    | Pode ignorar o estorno      | Processa cada transição uma única vez |

## `clientReference` na criação [#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.

<Mermaid
  chart="`
flowchart LR
  A[&#x22;POST /pix&#x22;]
  A -->|&#x22;1ª chamada&#x22;| B[&#x22;Cria nova&#x22;]
  A -->|&#x22;Retry&#x22;| C[&#x22;Devolve a existente&#x22;]

  click A &#x22;/docs/pix-processamento/endpoints/pix-operations/post_pix&#x22; &#x22;POST /pix&#x22;

  style B fill:#14ce71,stroke:#0eb464,color:#ffffff
  style C fill:#14ce71,stroke:#0eb464,color:#ffffff
`"
/>

### Como gerar [#como-gerar]

| Padrão                          | Quando 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.                           |

<Callout type="warn">
  **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.
</Callout>

```bash
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](/docs/pix-processamento/tutoriais/receive-pix).

## Dedupe de callbacks por `id + status` [#dedupe-de-callbacks-por-id--status]

O mesmo callback pode chegar mais de uma vez:

* **Retry de entrega**, a PayZu reenvia conforme a [política de retry dos webhooks](/docs/pix-processamento/webhooks#sistema-de-retry).
* **Mudanças sucessivas**, `PENDING → COMPLETED → REFUNDED`, cada uma gera callback.
* **Reprocessamento manual** via [`POST /user/callbacks/resend`](/docs/pix-processamento/endpoints/callbacks/resend_user_callbacks).

A chave de dedupe &#x2A;*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.

<Mermaid
  chart="`
flowchart LR
  A[&#x22;1ª chegada COMPLETED&#x22;] -->|&#x22;chave única&#x22;| K1[&#x22;Processa&#x22;]
  B[&#x22;Retry COMPLETED&#x22;] -->|&#x22;mesma chave&#x22;| K2[&#x22;Ignora&#x22;]
  C[&#x22;Mudança para REFUNDED&#x22;] -->|&#x22;chave nova&#x22;| K3[&#x22;Processa estorno&#x22;]

  click A &#x22;/docs/pix-processamento/webhooks&#x22; &#x22;Webhooks&#x22;
  click B &#x22;/docs/pix-processamento/webhooks#sistema-de-retry&#x22; &#x22;Retry&#x22;
  click C &#x22;/docs/pix-processamento/med&#x22; &#x22;MED estorno&#x22;

  style K1 fill:#14ce71,stroke:#0eb464,color:#ffffff
  style K2 fill:#737373,stroke:#525252,color:#ffffff
  style K3 fill:#ef4444,stroke:#dc2626,color:#ffffff
`"
/>

### Implementação [#implementação]

```ts
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 [#armadilhas-comuns]

| Armadilha                                                       | Sintoma                                      |
| --------------------------------------------------------------- | -------------------------------------------- |
| `clientReference` aleatório a cada retry                        | Cobrança duplicada, cliente confuso          |
| Dedupe usando só `id` (sem status)                              | Estorno não dá baixa, refund "fantasma"      |
| TTL do dedupe muito curto                                       | Retry 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 |
