Error handling
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 1async 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:
| 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. |
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[].
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.
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
Got the requestId saved? Send it straight to the team.