Error Handling & Resilience¶
Implement Retry with Exponential Backoff¶
Why: Transient failures (network timeouts, 500 errors, DNS hiccups) are inevitable. Retrying immediately in a tight loop can overwhelm the gateway and degrade your own performance. Exponential backoff spreads retries over time, giving the system a chance to recover.
Recommended approach:
- Retry only on transient errors: network timeouts,
500 Internal Server Error,502,503,429. - Use exponential backoff with jitter: wait
min(base * 2^attempt + random_jitter, max_delay). - Set a maximum number of retries (e.g., 3-5 attempts).
- Always reuse the same
merchantReferencewhen retrying a failed request — this ensures idempotency.
import time, random
# HTTP statuses that warrant a retry. Everything else short-circuits the loop.
RETRYABLE_STATUSES = {408, 429, 500, 502, 503, 504}
def call_with_retry(request_fn, max_retries=4, base_delay=1.0, max_delay=30.0):
for attempt in range(max_retries + 1):
try:
response = request_fn()
if response.status_code not in RETRYABLE_STATUSES:
return response
except (ConnectionError, Timeout):
if attempt == max_retries:
raise
delay = min(base_delay * (2 ** attempt) + random.uniform(0, 1), max_delay)
time.sleep(delay)
return response
Distinguish Retryable from Non-Retryable Errors¶
Why: Retrying a 400 Validation failed error with the same payload will always fail. Unnecessary retries waste time and may trigger rate limits.
V2 Error Response Structure¶
All V2 API errors follow RFC 7807 Problem Details with content type application/problem+json. The type field is a documentation URL pointing to the specific error code reference:
{
"type": "https://docs.payalo.com/errors/validation_failed",
"title": "Validation failed",
"status": 400,
"detail": "Country is not valid or not supported.",
"errorCode": "validation_failed"
}
V2 Error Code Reference¶
| HTTP Status | Error Code | Title | Description |
|---|---|---|---|
400 |
validation_failed |
Validation failed | A field failed validation rules |
400 |
bad_request |
Bad request | Request body is malformed or unreadable |
401 |
unauthorized |
Unauthorized | API key is missing or invalid |
404 |
not_found |
Not found | Requested resource does not exist |
422 |
business_logic_error |
Business logic error | Business rule violation (e.g., duplicate merchant reference) |
500 |
internal_server_error |
Internal server error | Unexpected server-side error |
When a validation error carries a specific business error code, the type URL and errorCode field reflect that specific code. See the full Error Code Reference for all codes.
Retryable vs Non-Retryable¶
| Category | HTTP Status | Error Code | Action |
|---|---|---|---|
| Retryable | 500 |
internal_server_error |
Retry with exponential backoff |
| Retryable | 502, 503, 429 |
— | Retry with exponential backoff |
| Non-retryable | 400 |
validation_failed, bad_request |
Fix the request; do not retry |
| Non-retryable | 401 |
unauthorized |
Verify your API key |
| Non-retryable | 404 |
not_found |
Resource does not exist |
| Non-retryable | 422 |
business_logic_error |
Fix the business logic violation |
Recommended approach:
- On
400/422: log the error, inspect thedetailanderrorCodefields, and fix the request before resubmitting. - On
401: verify your API key is correct and not expired. - On
500/503: retry with backoff. - On
429(if rate limiting is enforced): respect the backoff period before retrying. - Use the
typeURL to look up detailed documentation for each error code.
Handle Timeouts Gracefully¶
Why: A timeout does not mean the request failed — the gateway may have received and processed it. Assuming failure and creating a new transaction with a different merchantReference can result in duplicate charges.
Recommended approach:
- If a request times out, do not immediately create a new transaction.
- Retry with the same
merchantReference. The gateway's idempotency logic will return the existing transaction if it was already created. - If retries are exhausted, query the Status Check API using your
merchantReferenceto determine whether the transaction exists. - Only create a new transaction (with a new
merchantReference) if you confirm the original was never received.