> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ozura.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling

> Handle payment errors gracefully to give customers a smooth experience

Handle payment errors gracefully to give customers a smooth experience.

## Error Response Format

All API errors follow this structure:

```json theme={null}
{
  "success": false,
  "error": "Human-readable error message",
  "details": ["Specific validation error 1", "Specific validation error 2"]
}
```

## HTTP Status Codes

| Code  | Meaning             | What to Do                                        |
| :---- | :------------------ | :------------------------------------------------ |
| `400` | Bad Request         | Check your request parameters                     |
| `401` | Unauthorized        | Verify your API keys                              |
| `404` | Not Found           | Session doesn't exist                             |
| `409` | Conflict            | Session already completed/cancelled               |
| `410` | Gone                | Session expired                                   |
| `500` | Server Error        | Retry the request                                 |
| `502` | Bad Gateway         | Upstream service unavailable — retry with backoff |
| `503` | Service Unavailable | Service temporarily down — retry with backoff     |

## Two Types of Errors

Ozura Checkout handles errors differently based on whether the customer can fix them:

### Fixable Errors (Customer Stays on Checkout)

These errors allow the customer to try again without leaving the checkout page:

* Card declined
* Insufficient funds
* Invalid card number
* Expired card
* Wrong CVV
* Address verification failed

**What happens:** An inline error message appears. The customer can correct their information and try again.

### Unrecoverable Errors (Redirect to errorUrl)

These errors require redirecting the customer away from checkout:

* Server/system errors
* Authentication failures
* Network connectivity issues
* Payment processor unavailable

**What happens:** The customer is redirected to your `errorUrl` with error details in the URL.

## Redirect Behavior Summary

| Situation                     | What Happens                         | Session Status |
| :---------------------------- | :----------------------------------- | :------------- |
| Payment successful            | Redirect to `successUrl`             | `completed`    |
| Fixable error (card declined) | **Stays on checkout** – inline error | `pending`      |
| Unrecoverable error           | Redirect to `errorUrl`               | `failed`       |
| Customer cancels              | Redirect to `cancelUrl`              | `cancelled`    |
| Session expires               | Ozura expiration page → `cancelUrl`  | `expired`      |

## Error URL Parameters

When an unrecoverable error occurs, the customer is redirected to your `errorUrl`:

```
https://yoursite.com/error?success=false&errorCode=SYSTEM_ERROR&error=Payment+service+unavailable&cancelUrl=https://yoursite.com/cart
```

| Parameter   | Description                              | Example                          |
| :---------- | :--------------------------------------- | :------------------------------- |
| `success`   | Always `false`                           | `false`                          |
| `errorCode` | Machine-readable error type              | `SYSTEM_ERROR`, `SESSION_FAILED` |
| `error`     | Human-readable message                   | `Payment service unavailable`    |
| `cancelUrl` | Your cancelUrl (for "return" navigation) | `https://yoursite.com/cart`      |

### Error Codes

| Code             | Meaning                               |
| :--------------- | :------------------------------------ |
| `SYSTEM_ERROR`   | Server or connectivity issue          |
| `SESSION_FAILED` | Session marked as failed              |
| `MANUAL_REVIEW`  | Transaction flagged for manual review |
| `FORBIDDEN`      | Too many failed payment attempts      |

## Building Your Error Page

```javascript theme={null}
// Parse URL parameters
const params = new URLSearchParams(window.location.search);
const errorCode = params.get('errorCode');
const errorMessage = params.get('error');
const returnUrl = params.get('cancelUrl');

// Display to user
document.getElementById('error-message').textContent = 
  errorMessage || 'Something went wrong. Please try again.';

// Set up return button
if (returnUrl) {
  document.getElementById('return-btn').href = returnUrl;
}
```

## Recommended Approach: Redirect to Cart with Message

Instead of showing a dead-end error page, redirect customers back to their cart with a popup message:

**Set your errorUrl to your cart page with a flag:**

```
errorUrl: "https://yoursite.com/cart?showError=true"
```

**Then on your cart page:**

```javascript theme={null}
const params = new URLSearchParams(window.location.search);
if (params.get('showError')) {
  const errorMessage = params.get('error') || 'Something went wrong during checkout';
  showErrorPopup(errorMessage);
  
  // Clean up URL
  window.history.replaceState({}, '', '/cart');
}
```

This keeps customers in their shopping flow instead of showing a confusing error page.

## Retry Logic for API Calls

If you get a `500` error, it's safe to retry with exponential backoff:

```javascript theme={null}
async function createSessionWithRetry(data, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch('https://checkout.ozura.com/api/sessions/create', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-API-KEY': process.env.VAULT_API_KEY,
          'X-OZURA-API-KEY': process.env.MERCHANT_API_KEY
        },
        body: JSON.stringify(data)
      });
      
      if (response.ok) {
        return await response.json();
      }
      
      // Don't retry client errors (4xx)
      if (response.status < 500) {
        throw new Error(`Request failed: ${response.status}`);
      }
      
      // Wait before retrying (exponential backoff)
      await new Promise(r => setTimeout(r, 1000 * attempt));
      
    } catch (error) {
      if (attempt === maxRetries) throw error;
    }
  }
}
```

## Network Retry Protection

To prevent duplicate sessions from network issues, use an idempotency key:

```json theme={null}
{
  "merchantId": "your_merchant_id",
  "merchantName": "My Store",
  "amount": "25.00",
  "idempotencyKey": "order_12345"
}
```

If you send the same `idempotencyKey` twice, you'll get back the original session instead of creating a duplicate (the response will include `"cached": true`).

If the original session has expired or reached a terminal state (`cancelled` / `failed`), the replay is rejected with a `409` and one of these `errorCode` values — use a new `idempotencyKey` to create a fresh session:

| `errorCode`                        | Meaning                                                      |
| :--------------------------------- | :----------------------------------------------------------- |
| `IDEMPOTENCY_KEY_EXPIRED_SESSION`  | The session created with this key has expired                |
| `IDEMPOTENCY_KEY_TERMINAL_SESSION` | The session created with this key is `cancelled` or `failed` |
