# Error mapping (/docs/payment-orchestration/error-mapping)

## Precium API Error Reference [#precium-api-error-reference]

This document provides a comprehensive reference for all error codes, their causes, and recommended resolution steps.

## Error Response Format [#error-response-format]

All API errors follow a consistent JSON structure:

```json
{
  "__all__": {
    "message": "Descriptive error message",
    "code": "error_code"
  }
}
```

**Field-specific errors:**

```json
{
  "email": {
    "message": "Enter a valid email address",
    "code": "invalid"
  },
  "purchase.currency": {
    "message": "This field is required",
    "code": "required"
  }
}
```

## HTTP Status Codes [#http-status-codes]

| Status | Description | Common Causes |
|---|---|---|
| `200` | Success | Request completed successfully |
| `201` | Created | Resource created successfully |
| `204` | No Content | Successful deletion |
| `400` | Bad Request | Validation errors, malformed JSON |
| `401` | Unauthorized | Invalid or missing API key |
| `403` | Forbidden | Insufficient permissions |
| `404` | Not Found | Resource doesn't exist |
| `409` | Conflict | Duplicate request, state conflict |
| `422` | Unprocessable Entity | Business logic error |
| `429` | Too Many Requests | Rate limit exceeded |
| `500` | Internal Server Error | Server-side error |
| `502` | Bad Gateway | Upstream service unavailable |
| `503` | Service Unavailable | Temporary maintenance |

## Authentication Errors [#authentication-errors]

**`invalid_token`**

**HTTP Status:** 401

**Message:** Invalid or expired authentication token

**Causes:**

- API key is incorrect
- API key has been revoked
- Using sandbox key in production or vice versa

**Resolution:**

1. Verify your API key in the dashboard
2. Ensure you're using the correct environment
3. Generate a new API key if necessary

**`missing_authorization`**

**HTTP Status:** 401

**Message:** Authorization header is required

**Causes:**

- No Authorization header provided
- Malformed Authorization header

**Resolution:**

```bash
# Correct format
-H "Authorization: Bearer YOUR_API_KEY"

# Common mistakes
-H "Authorization: YOUR_API_KEY"  # Missing "Bearer"
-H "Bearer YOUR_API_KEY"          # Missing "Authorization:"
```

## Validation Errors [#validation-errors]

**`required`**

**HTTP Status:** 400

**Message:** This field is required

**Example:**

```json
{
  "brand_id": {
    "message": "This field is required",
    "code": "required"
  }
}
```

**Resolution:** Include all required fields in your request.

**`invalid`**

**HTTP Status:** 400

**Message:** Invalid value for this field

**Common Cases:**

| Field | Valid Format | Example Error |
|---|---|---|
| `email` | Valid email | `invalid_email@` |
| `currency` | ISO 4217 (3 chars) | `RAND` instead of `ZAR` |
| `phone` | E.164 format | `0821234567` instead of `+27821234567` |
| `country` | ISO 3166-1 alpha-2 | `South Africa` instead of `ZA` |

**`invalid_uuid`**

**HTTP Status:** 400

**Message:** Invalid UUID format

**Causes:**

- `client_id` or `brand_id` is not a valid UUID
- UUID contains invalid characters

**Valid UUID format:**

```text
3fa85f64-5717-4562-b3fc-2c963f66afa6
```

**`max_length`**

**HTTP Status:** 400

**Message:** Ensure this field has no more than X characters

**Field Limits:**

| Field | Max Length |
|---|---|
| `reference` | 128 |
| `purchase.products[].name` | 256 |
| `success_redirect` | 500 |
| `cardholder_name` | 30 |
| `card_number` | 19 |

**`min_value` / `max_value`**

**HTTP Status:** 400

**Message:** Ensure this value is greater/less than X

**Amount Limits:**

| Field | Min | Max |
|---|---|---|
| `purchase.products[].price` | 0 | 9,999,999,999 |
| `purchase.total_override` | 0 | 999,999,999,999,999 |

## Purchase Errors [#purchase-errors]

**`purchase_not_found`**

**HTTP Status:** 404

**Message:** Purchase not found

**Causes:**

- Invalid purchase ID
- The purchase belongs to a different merchant
- Purchase has been deleted

**Resolution:**

1. Verify the purchase ID is correct
2. Ensure you're using the correct API key

**`purchase_already_paid`**

**HTTP Status:** 409

**Message:** Purchase has already been paid

**Causes:**

- Attempting to charge an already-completed purchase
- Duplicate payment submission

**Resolution:**

- Check purchase status before charging
- Implement idempotency in your integration

**`purchase_cancelled`**

**HTTP Status:** 409

**Message:** Purchase has been cancelled

**Causes:**

- Attempting to charge a cancelled purchase
- Purchase expired and was auto-cancelled

**Resolution:**

- Create a new purchase for the transaction

**`purchase_expired`**

**HTTP Status:** 409

**Message:** Purchase has expired

**Causes:**

- `due` the date has passed with `due_strict: true`
- Direct post URL has expired (15-minute validity)

**Resolution:**

- Create a new purchase
- Process payments promptly after creation

**`invalid_amount`**

**HTTP Status:** 400

**Message:** Invalid amount specified

**Causes:**

- Negative amount
- Amount exceeds original purchase
- Zero amount for non-authorization purchase

**Resolution:**

- Ensure the amount is positive
- For partial captures/refunds, ensure the amount ≤ remaining

**`capture_not_allowed`**

**HTTP Status:** 409

**Message:** Capture is not allowed for this purchase

**Causes:**

- Purchase was not created with `skip_capture: true`
- Purchase is not in `hold` status
- Authorization has expired

**Resolution:**

- Verify the purchase status is `hold`
- Create a new purchase with `skip_capture: true` for pre-auth

**`refund_exceeds_amount`**

**HTTP Status:** 400

**Message:** Refund amount exceeds available balance

**Causes:**

- Refund amount greater than the original payment
- Previous refunds have reduced the available balance

**Resolution:**

```python
# Check available refund amount
purchase = get_purchase(purchase_id)
available = purchase['purchase']['total'] - purchase.get('refunded_amount', 0)
```

## Payment Method Errors [#payment-method-errors]

**`payment_method_not_available`**

**HTTP Status:** 400

**Message:** Payment method not available

**Causes:**

- Payment method not enabled for your merchant
- Payment method not supported for the currency
- Payment method excluded by `payment_method_whitelist`

**Resolution:**

1. Check the enabled payment methods
2. Verify currency support for the payment method
3. Review `payment_method_whitelist` in request

**`recurring_token_not_found`**

**HTTP Status:** 404

**Message:** Recurring token not found

**Causes:**

- Invalid token ID
- Token has been deleted
- The token belongs to a different client

**Resolution:**

1. List client tokens: `GET /clients/{id}/recurring_tokens/`
2. Verify the token ID is correct
3. Re-tokenize if necessary

**`recurring_token_expired`**

**HTTP Status:** 409

**Message:** Recurring token has expired

**Causes:**

- The card has expired
- Token validity period exceeded
- Mandate expired (DebiCheck)

**Resolution:**

- Request the customer to re-tokenize payment method

**`card_declined`**

**HTTP Status:** 422

**Message:** Card was declined

**Decline Reasons:**

| Code | Description | Customer Action |
|---|---|---|
| `do_not_honour` | Generic decline | Try different card |
| `insufficient_funds` | Not enough balance | Add funds or use different card |
| `expired_card` | Card has expired | Use valid card |
| `invalid_card` | Card number invalid | Check card details |
| `lost_stolen` | Card reported lost/stolen | Contact bank |
| `suspected_fraud` | Fraud suspicion | Contact bank |

**`3ds_authentication_failed`**

**HTTP Status:** 422

**Message:** 3D Secure authentication failed

**Causes:**

- Customer failed 3DS challenge
- 3DS timeout
- Customer cancelled authentication

**Resolution:**

- The customer should retry with the correct credentials
- Ensure the 3DS page loads correctly

## Client Errors [#client-errors]

**`client_not_found`**

**HTTP Status:** 404

**Message:** Client not found

**Resolution:**

1. Verify client ID is correct
2. Create a client if it doesn't exist

`duplicate_client`

**HTTP Status:** 409

**Message:** Client with this email already exists

**Causes:**

- Attempting to create a client with an existing email

**Resolution:**

- Search for existing client: `GET /clients/?email=customer@example.com`
- Use existing client ID

## Billing Template Errors [#billing-template-errors]

**`billing_template_not_found`**

**HTTP Status:** 404

**Message:** Billing template not found

**`subscriber_already_exists`**

**HTTP Status:** 409

**Message:** Client is already subscribed to this template

**Resolution:**

- Check existing subscription status
- Update existing subscription instead of creating new one

**`subscription_inactive`**

**HTTP Status:** 409

**Message:** Subscription is not active

**Causes:**

- `subscription_active: false` on template
- Subscription has been cancelled

## Webhook Errors [#webhook-errors]

**`webhook_url_invalid`**

**HTTP Status:** 400

**Message:** Invalid webhook URL

**Requirements:**

- Must be HTTPS
- Must be publicly accessible
- Must respond within 5 seconds

**`webhook_url_unreachable`**

**HTTP Status:** 400

**Message:** Webhook URL is not reachable

**Resolution:**

1. Verify URL is publicly accessible
2. Check firewall rules
3. Ensure the SSL certificate is valid

## Rate Limiting Errors [#rate-limiting-errors]

**`rate_limit_exceeded`**

**HTTP Status:** 429

**Message:** Rate limit exceeded

**Response Headers:**

```text
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1619740860
```

**Resolution:**

1. Implement exponential backoff
2. Cache responses where possible
3. Batch operations when available

**Retry Logic:**

```python
import time

def api_request_with_retry(func, max_retries=3):
    for attempt in range(max_retries):
        response = func()
        if response.status_code != 429:
            return response

        reset_time = int(response.headers.get('X-RateLimit-Reset', 0))
        wait_time = max(reset_time - time.time(), 2 ** attempt)
        time.sleep(wait_time)

    raise Exception("Rate limit exceeded after retries")
```

## Direct Post Errors [#direct-post-errors]

**`direct_post_expired`**

**HTTP Status:** 400

**Message:** Direct post URL has expired

**Causes:**

- URL validity is 15 minutes
- URL has already been used

**Resolution:**

- Create a new purchase for the fresh direct post URL

**`invalid_card_data`**

**HTTP Status:** 400

**Message:** Invalid card data submitted

**Field Validation:**

| Field | Validation | Error |
|---|---|---|
| `cardholder_name` | Latin letters, space, apostrophe, dot, dash | Non-Latin characters |
| `card_number` | Digits only, valid Luhn | Invalid format |
| `expires` | MM/YY format, future date | Past date or invalid format |
| `cvc` | 3-4 digits | Invalid length |

## Error Handling Best Practices [#error-handling-best-practices]

**1. Always Check HTTP Status**

```python
response = requests.post(url, json=data, headers=headers)

if response.status_code == 201:
    # Success - process response
    purchase = response.json()
elif response.status_code == 400:
    # Validation error - show user-friendly message
    errors = response.json()
    handle_validation_errors(errors)
elif response.status_code == 401:
    # Auth error - check credentials
    refresh_credentials()
elif response.status_code == 429:
    # Rate limited - retry with backoff
    retry_with_backoff()
else:
    # Other error - log and alert
    log_error(response)
```

**2. Display User-Friendly Messages**

```python
ERROR_MESSAGES = {
    'card_declined': 'Your card was declined. Please try a different payment method.',
    'insufficient_funds': 'Insufficient funds. Please use a different card.',
    'expired_card': 'Your card has expired. Please use a valid card.',
    '3ds_authentication_failed': 'Authentication failed. Please try again.',
    'invalid': 'Please check your information and try again.'
}

def get_user_message(error_code):
    return ERROR_MESSAGES.get(error_code, 'An error occurred. Please try again.')
```

**3. Log Errors for Debugging**

```python
import logging

def log_api_error(response, request_data):
    logging.error(
        "API Error",
        extra={
            'status_code': response.status_code,
            'response_body': response.text,
            'request_url': response.url,
            'request_data': sanitize_sensitive_data(request_data)
        }
    )
```

**4. Implement Retry Logic**

```python
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=2, max=10),
    retry=lambda e: isinstance(e, RetryableError)
)
def make_api_request():
    response = requests.post(...)
    if response.status_code in [429, 502, 503]:
        raise RetryableError()
    return response
```

## Error Code Quick Reference [#error-code-quick-reference]

| Code | HTTP | Category | Action |
|---|---|---|---|
| `invalid_token` | 401 | Auth | Check API key |
| `required` | 400 | Validation | Add missing field |
| `invalid` | 400 | Validation | Fix field format |
| `purchase_not_found` | 404 | Resource | Verify ID |
| `purchase_already_paid` | 409 | State | Check status first |
| `card_declined` | 422 | Payment | Try different card |
| `insufficient_funds` | 422 | Payment | Try different card |
| `3ds_authentication_failed` | 422 | Payment | Retry authentication |
| `recurring_token_expired` | 409 | Token | Re-tokenize |
| `rate_limit_exceeded` | 429 | Rate Limit | Implement backoff |
