最佳实践
错误处理
PayZu 返回标准 HTTP 状态码。你的策略取决于错误类别。
处理对照表
| 状态码 | 含义 | 处理方式 |
|---|---|---|
200 | 操作成功。 | 处理响应。 |
201 | 资源已创建(收款、提现)。 | 处理响应。 |
400 | Payload 无效。 | 不要重试。记录 message + requestId 并修正代码。 |
401 | Token 缺失、无效或已撤销。 | 不要重试。检查 token(空格、编码、轮换)。 |
403 | Token 有效但没有该 endpoint 的权限。 | 不要重试。联系支持团队确认账户是否已开通。 |
404 | 资源未找到。 | 不要重试。确认 id 或 clientReference。 |
409 | 冲突(重复、资源状态无效)。 | 不要重试。重试前通过 GET 查询当前状态。 |
422 | 语义校验失败。 | 不要重试。根据 message 修正 payload。 |
429 | 触发 rate limit。 | 等待,做指数退避后再重试。 |
5xx | PayZu 服务器错误。 | 带指数退避重试(1s → 2s → 4s → 8s,最多 4 次)。 |
| Timeout | 在期限内没有响应。 | 操作可能已被应用。重新创建前通过 clientReference 查询。 |
Retry helper
只在 429 和 5xx 时重试。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 ;;
429|5*) sleep $DELAY; DELAY=$((DELAY*2)) ;;
*) echo "错误 $STATUS"; cat /tmp/resp.json; exit 1 ;;
esac
done
echo "已超过最大重试次数"
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) {
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');
}import time, random, requests
def with_retry(fn, attempts=4):
last_err = None
for i in range(attempts):
try:
res = fn()
if res.ok:
return res.json()
if 400 <= res.status_code < 500 and res.status_code != 429:
raise RuntimeError(f'Client error {res.status_code}: {res.text}')
except Exception as e:
last_err = e
delay = min(8.0, 1.0 * (2 ** i)) + random.random() * 0.25
time.sleep(delay)
raise last_err or RuntimeError('Max retries exceeded')func WithRetry(fn func() (*http.Response, error), attempts int) ([]byte, error) {
var lastErr error
for i := 0; i < attempts; i++ {
res, err := fn()
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
return io.ReadAll(res.Body)
}
if res.StatusCode >= 400 && res.StatusCode < 500 && res.StatusCode != 429 {
body, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("client error %d: %s", res.StatusCode, body)
}
}
lastErr = err
delay := time.Duration(math.Min(8000, 1000*math.Pow(2, float64(i)))) * time.Millisecond
time.Sleep(delay + time.Duration(rand.Intn(250))*time.Millisecond)
}
return nil, lastErr
}Timeout:“可能已经成功”的陷阱
Timeout 不等同于失败。PayZu 可能已经接收、处理并保存了交易,只是响应没有返回。你的应用并不知道。
解决方案:使用唯一的 clientReference,重试前先查询。
async function createOrRetry(orderId: string, amount: number) {
const ref = `order-${orderId}`;
try {
return await withRetry(() => postPix({ amount, clientReference: ref }));
} catch (err) {
// 尽管出错/超时,操作可能已经成功
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;
}
}错误的可观测性
至少要记录以下字段:
| 字段 | 原因 |
|---|---|
requestId | 来自 PayZu 错误响应。支持团队可直接追踪。 |
本地 id | 你的标识符(订单、提现)。 |
PayZu id | 如果已存在。 |
endToEndId | 在争议时用于在 Bacen 追踪。 |
clientReference | 通用的关联键。 |
| HTTP status + message | 根本原因几乎总是在 message 中。 |
| 第 N 次 / 共 M 次 | 区分首次尝试与重试。 |
log.error('PayZu /pix 调用失败', {
requestId: body.requestId,
status: res.status,
message: body.message,
clientReference: ref,
attempt: i + 1,
attempts,
});给终端用户的友好错误提示
不要直接暴露原始的 message。将其翻译为可操作的提示:
| PayZu 错误 | 给用户的提示 |
|---|---|
401 Unauthorized | “配置错误。请联系支持团队并提供 requestId 代码。” |
400 amount must be >= 1 | “收款最低金额为 R$ 1,00。” |
400 invalid pixKey | “Pix 密钥无效。请核对后重试。” |
429 Too Many Requests | “请求过多。请稍后再试。” |
5xx | “系统暂时不可用。我们正在处理。” |
常见陷阱
| 陷阱 | 症状 |
|---|---|
在 400 上重试 | 对 API 形成 spam,相同错误重复 N 次 |
在 401 上重试却不轮换 token | Token 在日志中进一步泄漏 |
| 没有退避(立即循环重试) | 变成 rate limit,最终被封禁 |
| 退避没有 jitter | N 个客户端同时打过来,形成 “thundering herd” |
| 把 timeout 当作最终失败 | 向用户重复扣款 2 次 |
不记录 requestId | 支持团队无法调查 |
使用 requestId 开支持工单
保存了 requestId?直接发给支持团队。
Consulta DICT
O DICT é o banco central do Bacen que guarda todas as chaves Pix registradas no Brasil. Consultar antes de pagar valida que a chave existe, mostra o titular para confirmação e reduz pagamentos para destinatários errados.
Checklist de produção
Lista verificável dos itens que sua integração precisa antes de receber tráfego real. Cobre idempotência, callbacks, dinheiro, segurança, conciliação e observabilidade.