# 

## Types of payment failures

Payment failures fall into three categories:

| Category | Cause | `response_status` | Retryable? |
|  --- | --- | --- | --- |
| **Decline** | Issuer refused the transaction | `declined` | Usually no — ask for a different card |
| **Error** | Processor or gateway issue | `error` | Yes — retry with backoff |
| **Network failure** | Timeout, DNS, connection reset | (no response) | Yes — retry with backoff |


## Failure decision tree

![image](https://files.modern-mermaid.live/images/1776277334221-mermaid-diagram-1776277334330.png)

## Retry strategy

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

Python
```python
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"}
```

JavaScript
```javascript
const API_KEY = process.env.PARADISE_API_KEY;
const BASE = "https://api.dev.paradisegateway.net";

async function sendTransaction(payload, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), 30000);

      const resp = await fetch(`${BASE}/v1/transactions`, {
        method: "POST",
        headers: { "Content-Type": "application/json", "APIKEY": API_KEY },
        body: JSON.stringify(payload),
        signal: controller.signal,
      });
      clearTimeout(timer);
      const result = await resp.json();

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

      if (result.response_status === "declined") {
        return {
          success: false, retryable: false,
          reason: result.response_message || "Declined",
          correlationId: result.correlation_id,
        };
      }

      if (resp.status >= 400 && resp.status < 500) {
        return {
          success: false, retryable: false,
          reason: `Client error: ${resp.status}`,
          correlationId: result.correlation_id,
        };
      }
    } catch (err) {
      // timeout or network error — will retry
    }

    const wait = 2 ** attempt * 1000 + Math.random() * 1000;
    console.log(`Attempt ${attempt + 1} failed, retrying in ${(wait / 1000).toFixed(1)}s...`);
    await new Promise((r) => setTimeout(r, 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.

```python
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 type | User-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 status | `response_status` | Action |
|  --- | --- | --- |
| 200 | `approved` | Fulfill order |
| 200 | `declined` | Ask for different payment method |
| 200 | `error` | Retry with backoff |
| 400 | — | Fix the request (missing/invalid fields) |
| 401 | — | Check API key |
| 404 | — | Check the endpoint or resource ID |
| 422 | — | Validation failed (for example, invalid card number) |
| 5xx | — | Retry with backoff |
| (no response) | — | Retry with backoff |


## Next steps

* [Handle declined transactions](/docs/tutorials/payment-cookbook/handle-declined-transactions) — detailed decline handling
* [ACH returns](/docs/tutorials/payment-cookbook/auth-returns) — ACH-specific failure patterns
* [Error handling concepts](/docs/concepts/api-fundamentals/error-handling)
* [Integration best practices](/docs/get-started/integration-best-practices)