Error handling
Saq returns standard HTTP codes. Your strategy depends on the category.
Handling table
| Code | What it means | What to do |
|---|---|---|
200 | Operation succeeded. | Process the response. |
201 | Resource created (charge, withdrawal). | Process the response. |
400 | Invalid payload. | Do not retry. Log message + requestId and fix the code. |
401 | Missing, invalid or revoked token. | Do not retry. Check the token (whitespace, encoding, rotation). |
403 | Valid token but no permission for the endpoint. | Do not retry. Contact support to validate account enablement. |
404 | Resource not found. | Do not retry. Confirm id or clientReference. |
409 | Conflict (duplicate, resource in invalid state). | Do not retry. Query the current state via GET before trying again. |
422 | Semantic validation failed. | Do not retry. Fix the payload according to message. |
429 | Rate limit reached. | Wait, apply exponential backoff and try again. |
5xx | Saq server error. | Retry with exponential backoff (1s → 2s → 4s → 8s, max 4 attempts). |
| Timeout | No response within the deadline. | The operation may have been applied. Query by clientReference before recreating. |
Retry helper
Retry only on 429 and 5xx. Never on 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.saq.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 "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) {
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: the "it might have worked" trap
A timeout is not equivalent to a failure. Saq 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.saq.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 Saq error responses. Support traces it directly. |
Local id | Your identifier (order, withdrawal). |
Saq 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('Saq /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. Translate it into something actionable:
| Saq error | Message for the user |
|---|---|
401 Unauthorized | "Configuration error. Contact support with the requestId code." |
400 amount must be >= 1 | "Minimum charge amount is R$ 1.00." |
400 invalid pixKey | "Invalid Pix key. Check it and try again." |
429 Too Many Requests | "We have too many requests. Try again shortly." |
5xx | "System temporarily unavailable. We are already looking into it." |
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.
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.