Skip to content

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 merchantReference when 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 the detail and errorCode fields, 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 type URL 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:

  1. If a request times out, do not immediately create a new transaction.
  2. Retry with the same merchantReference. The gateway's idempotency logic will return the existing transaction if it was already created.
  3. If retries are exhausted, query the Status Check API using your merchantReference to determine whether the transaction exists.
  4. Only create a new transaction (with a new merchantReference) if you confirm the original was never received.