# Error handling (/en/docs/pix-processamento/best-practices/errors)

<QuickLinks>
  <QuickLink href="/docs/pix-processamento/error-codes" title="Error codes" />

  <QuickLink href="/docs/pix-processamento/authentication" title="Authentication" />

  <QuickLink href="/docs/pix-processamento/best-practices/idempotency" title="Idempotency" />
</QuickLinks>

PayZu returns standard HTTP codes. Your strategy depends on the category.

<Mermaid
  chart="`
flowchart TD
  A[&#x22;PayZu call&#x22;]
  A --> B{&#x22;Status code&#x22;}
  B -->|&#x22;2xx&#x22;| OK[&#x22;Success&#x22;]
  B -->|&#x22;4xx (except 424/429)&#x22;| C[&#x22;Payload error<br/>DO NOT retry&#x22;]
  B -->|&#x22;424&#x22;| G[&#x22;Financial institution<br/>retry with backoff&#x22;]
  B -->|&#x22;429&#x22;| D[&#x22;Rate limit<br/>exponential backoff&#x22;]
  B -->|&#x22;5xx&#x22;| E[&#x22;PayZu error<br/>retry with backoff&#x22;]
  B -->|&#x22;Timeout&#x22;| F[&#x22;Operation may<br/>have been applied&#x22;]

  click C &#x22;/en/docs/pix-processamento/error-codes&#x22; &#x22;Error codes&#x22;
  click D &#x22;#retry-helper&#x22; &#x22;Retry helper&#x22;
  click G &#x22;#retry-helper&#x22; &#x22;Retry helper&#x22;
  click E &#x22;#retry-helper&#x22; &#x22;Retry helper&#x22;
  click F &#x22;#timeout-the-it-might-have-worked-trap&#x22; &#x22;Timeout trap&#x22;

  style OK fill:#14ce71,stroke:#0eb464,color:#ffffff
  style C fill:#ef4444,stroke:#dc2626,color:#ffffff
  style G fill:#f59e0b,stroke:#d97706,color:#ffffff
  style D fill:#f59e0b,stroke:#d97706,color:#ffffff
  style E fill:#f59e0b,stroke:#d97706,color:#ffffff
  style F fill:#f59e0b,stroke:#d97706,color:#ffffff
`"
/>

The complete table of HTTP codes and the `errorCode` catalog, with what to do for each, are in [Error codes](/docs/pix-processamento/error-codes); this page covers the strategy: when to retry, how to back off and what to log.

## Retry helper [#retry-helper]

Retry on `429`, `5xx` and `424`. **Never** on the other `4xx`.

<Tabs items="['curl', 'Node.js']">
  <Tab value="curl">
    ```bash
    ATTEMPTS=4
    DELAY=1

    for i in $(seq 1 $ATTEMPTS); do
      STATUS=$(curl -s -o /tmp/resp.json -w "%{http_code}" \
        -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"}')

      case $STATUS in
        2*) cat /tmp/resp.json; exit 0 ;;
        424|429|5*) sleep $DELAY; DELAY=$((DELAY*2)) ;;
        *) echo "Error $STATUS"; cat /tmp/resp.json; exit 1 ;;
      esac
    done
    echo "Max retries exceeded"
    exit 1
    ```
  </Tab>

  <Tab value="Node.js">
    ```ts
    async function withRetry<T>(
      fn: () => Promise<Response>,
      attempts = 4,
    ): Promise<T> {
      let lastErr: unknown;
      for (let i = 0; i < attempts; i++) {
        try {
          const res = await fn();
          if (res.ok) return res.json();

          if (res.status >= 400 && res.status < 500 && res.status !== 429 && res.status !== 424) {
            const body = await res.text();
            throw new Error(`Client error ${res.status}: ${body}`);
          }
        } catch (err) {
          lastErr = err;
        }

        const delay = Math.min(8000, 1000 * 2 ** i) + Math.random() * 250;
        await new Promise((r) => setTimeout(r, delay));
      }
      throw lastErr ?? new Error('Max retries exceeded');
    }
    ```
  </Tab>
</Tabs>

## Timeout: the "it might have worked" trap [#timeout-the-it-might-have-worked-trap]

A timeout is not equivalent to a failure. PayZu may have received, processed and persisted the transaction, and only the response failed to come back. Your app does not know.

**Solution**: use a unique `clientReference` and query before retrying.

```ts
async function createOrRetry(orderId: string, amount: number) {
  const ref = `order-${orderId}`;
  try {
    return await withRetry(() => postPix({ amount, clientReference: ref }));
  } catch (err) {
    // it may have succeeded despite the error/timeout
    const existing = await fetch(
      `https://api.payzu.processamento.com/v1/pix?clientReference=${ref}`,
      { headers },
    ).then((r) => (r.ok ? r.json() : null));
    if (existing) return existing;
    throw err;
  }
}
```

## Error observability [#error-observability]

Always log, at a minimum:

| Field                 | Why                                                         |
| --------------------- | ----------------------------------------------------------- |
| `requestId`           | Comes in PayZu error responses. Support traces it directly. |
| Local `id`            | Your identifier (order, Pix payment).                       |
| PayZu `id`            | If one already exists.                                      |
| `endToEndId`          | Useful for tracing at Bacen in a dispute.                   |
| `clientReference`     | The universal correlation key.                              |
| HTTP status + message | The root cause is almost always in `message`.               |
| Attempt N of M        | Tells a first attempt apart from a retry.                   |

```ts
log.error('PayZu /pix failed', {
  requestId: body.requestId,
  status: res.status,
  message: body.message,
  clientReference: ref,
  attempt: i + 1,
  attempts,
});
```

## Useful error messages for the end user [#useful-error-messages-for-the-end-user]

Do not expose `message` raw. Match on `errorCode`, which is stable, not on the text: `message` comes in Portuguese and can change. A schema validation failure always arrives as `PZV001`, with the offending field in `details[]`.

| `errorCode`         | HTTP | Message for the user                                                    |
| ------------------- | ---- | ----------------------------------------------------------------------- |
| `PZA100`            | 401  | "Configuration error. Contact support with the `requestId` code."       |
| `PZV001`            | 400  | Build the message from `details[].field`, field by field.               |
| `PZD600`            | 400  | "Amount below the minimum accepted for this operation."                 |
| `PZC200`            | 422  | "Insufficient balance to complete the operation."                       |
| `PZI110` / `PZI111` | 424  | "The financial institution is unstable. Try again in a few moments."    |
| `PZF500`            | 424  | "The financial institution is unavailable. Try again in a few moments." |
| `PZG429`            | 429  | "We have too many requests. Try again shortly."                         |
| `PZI100`            | 500  | "System temporarily unavailable. We are already looking into it."       |

The full catalog is in [Error codes](/docs/pix-processamento/error-codes).

## Common pitfalls [#common-pitfalls]

| Pitfall                                      | Symptom                                           |
| -------------------------------------------- | ------------------------------------------------- |
| Retrying on `400`                            | Spamming the API, same error N times              |
| Retrying on `401` without rotating the token | Token leaks even more into the log                |
| No backoff (immediate retry in a loop)       | Becomes rate limited, then gets banned            |
| No jitter in the backoff                     | N clients hit at the same time, "thundering herd" |
| Treating a timeout as a definitive failure   | Customer is charged twice                         |
| Not logging `requestId`                      | Support cannot investigate                        |

## Open a support ticket with the `requestId` [#open-a-support-ticket-with-the-requestid]

Got the `requestId` saved? Send it straight to the team.

<QuickLinks>
  <QuickLink href="https://suporte.payzu.com.br/portal/pt-br/newticket?departmentId=1103699000000006907&layoutId=1103699000000074011" title="Open ticket" />

  <QuickLink href="https://suporte.payzu.com.br/portal/pt-br/kb/payzu" title="Knowledge base" />

  <QuickLink href="https://suporte.payzu.com.br" title="Support portal" />
</QuickLinks>