PayZuDocs
Best practices

Explains which errors allow a retry, which do not, and how to handle a timeout.

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

The complete table of HTTP codes and the errorCode catalog, with what to do for each, are in Error codes; this page covers the strategy: when to retry, how to back off and what to log.

Retry helper

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

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
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');
}

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.

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

Always log, at a minimum:

FieldWhy
requestIdComes in PayZu error responses. Support traces it directly.
Local idYour identifier (order, Pix payment).
PayZu idIf one already exists.
endToEndIdUseful for tracing at Bacen in a dispute.
clientReferenceThe universal correlation key.
HTTP status + messageThe root cause is almost always in message.
Attempt N of MTells a first attempt apart from a retry.
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

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[].

errorCodeHTTPMessage for the user
PZA100401"Configuration error. Contact support with the requestId code."
PZV001400Build the message from details[].field, field by field.
PZD600400"Amount below the minimum accepted for this operation."
PZC200422"Insufficient balance to complete the operation."
PZI110 / PZI111424"The financial institution is unstable. Try again in a few moments."
PZF500424"The financial institution is unavailable. Try again in a few moments."
PZG429429"We have too many requests. Try again shortly."
PZI100500"System temporarily unavailable. We are already looking into it."

The full catalog is in Error codes.

Common pitfalls

PitfallSymptom
Retrying on 400Spamming the API, same error N times
Retrying on 401 without rotating the tokenToken leaks even more into the log
No backoff (immediate retry in a loop)Becomes rate limited, then gets banned
No jitter in the backoffN clients hit at the same time, "thundering herd"
Treating a timeout as a definitive failureCustomer is charged twice
Not logging requestIdSupport cannot investigate

Open a support ticket with the requestId

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

On this page