Skip to content
Last updated

Recipe: Handle payment failures

Types of payment failures

Payment failures fall into three categories:

CategoryCauseresponse_statusRetryable?
DeclineIssuer refused the transactiondeclinedUsually no — ask for a different card
ErrorProcessor or gateway issueerrorYes — retry with backoff
Network failureTimeout, DNS, connection reset(no response)Yes — retry with backoff

Failure decision tree

image

Retry strategy

Use exponential backoff with jitter to avoid overwhelming the API during outages.

import time, random, requests, os

API_KEY = os.environ["PARADISE_API_KEY"]
BASE = "https://api.dev.paradisegateway.net"

def send_transaction(payload, max_retries=3):
    for attempt in range(max_retries):
        try:
            resp = requests.post(
                f"{BASE}/v1/transactions",
                headers={
                    "Content-Type": "application/json",
                    "APIKEY": API_KEY,
                },
                json=payload,
                timeout=30,
            )
            result = resp.json()

            if result.get("response_status") == "approved":
                return {"success": True, "transaction": result}

            if result.get("response_status") == "declined":
                return {
                    "success": False,
                    "retryable": False,
                    "reason": result.get("response_message", "Declined"),
                    "correlation_id": result.get("correlation_id"),
                }

            if resp.status_code >= 400 and resp.status_code < 500:
                return {
                    "success": False,
                    "retryable": False,
                    "reason": f"Client error: {resp.status_code}",
                    "correlation_id": result.get("correlation_id"),
                }

        except requests.exceptions.Timeout:
            pass  # will retry
        except requests.exceptions.ConnectionError:
            pass  # will retry

        wait = (2 ** attempt) + random.uniform(0, 1)
        print(f"Attempt {attempt + 1} failed, retrying in {wait:.1f}s...")
        time.sleep(wait)

    return {"success": False, "retryable": False, "reason": "Max retries exceeded"}

Logging failures

Log every failure with the correlation_id so support can trace the request.

import logging

logger = logging.getLogger("payments")

def log_failure(result, payload):
    logger.error(
        "Payment failed: reason=%s correlation_id=%s amount=%s type=%s",
        result.get("reason"),
        result.get("correlation_id"),
        payload.get("amount"),
        payload.get("type"),
    )

What to show the customer

Failure typeUser-facing message
Decline"Your payment was declined. Please try a different payment method."
Temporary error"We're having trouble processing your payment. Please try again."
Timeout (all retries exhausted)"We couldn't reach the payment processor. Please try again later."
Validation error (4xx)"There's an issue with your payment details. Please check and retry."
Security

Never expose response_code, response_message, processor details, or stack traces to the customer. Log them server-side and show generic, friendly messages.

Failure summary table

HTTP statusresponse_statusAction
200approvedFulfill order
200declinedAsk for different payment method
200errorRetry with backoff
400Fix the request (missing/invalid fields)
401Check API key
404Check the endpoint or resource ID
422Validation failed (for example, invalid card number)
5xxRetry with backoff
(no response)Retry with backoff

Next steps