# Troubleshooting (/docs/server-to-server/troubleshooting)

## Precium S2S Troubleshooting Guide [#precium-s2s-troubleshooting-guide]

This guide helps you diagnose and resolve common issues when integrating with the Precium S2S API.

## Diagnostic Decision Tree [#diagnostic-decision-tree]

Use this flowchart to identify your issue category:

```mermaid
flowchart TD
    A[Issue Encountered] --> B{Error in Response?}
    B -->|Yes| C{HTTP Status?}
    B -->|No| D{Unexpected Behavior?}
    C -->|4xx| E[Client Error]
    C -->|5xx| F[Server Error]
    E --> G{Status Code}
    G -->|400| H[Bad Request - Check payload]
    G -->|401| I[Auth Error - Check API key]
    G -->|403| J[Forbidden - Check permissions]
    G -->|404| K[Not Found - Check endpoint/ID]
    G -->|422| L[Validation - Check data format]
    F --> M{Is it intermittent?}
    M -->|Yes| N[Retry with backoff]
    M -->|No| O[Contact support]
    D -->|3DS not completing| P[3DS Issues]
    D -->|Webhooks not arriving| Q[Webhook Issues]
    D -->|Charges failing| R[Payment Issues]
```

## Authentication Issues [#authentication-issues]

### Issue: "401 Unauthorized" Response [#issue-401-unauthorized-response]

**Symptoms:**

```json
{
  "error": "Invalid or missing API key"
}
```

**Solutions:**

| Cause | Solution |
|---|---|
| Missing Authorization header | Add `Authorization: Bearer ` to request |
| Using wrong key type | Use Standard key for most endpoints, S2S key for direct post |
| API key has whitespace | Trim whitespace from key |
| API key expired or revoked | Generate new key in dashboard |
| Using test key in production | Switch to live keys |

**Verification:**

```bash
curl -X GET https://gate.reviopay.com/api/v1/clients/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"
```

### Issue: "403 Forbidden" Response [#issue-403-forbidden-response]

**Common Causes:**

- S2S not enabled for your account
- Attempting operations outside your permission scope
- IP restrictions in place

**Solution:** Contact Precium support to verify your account permissions.

## Request Validation Issues [#request-validation-issues]

### Issue: "400 Bad Request" or "422 Unprocessable Entity" [#issue-400-bad-request-or-422-unprocessable-entity]

**Check these common mistakes:**

**Missing Required Fields**

```json
// Wrong
{
  "currency": "ZAR",
  "products": [{"name": "Test", "price": 100}]
}

// Correct
{
  "client_id": "uuid-here",
  "brand_id": "your-brand-id",
  "currency": "ZAR",
  "products": [{"name": "Test", "price": 100}],
  "success_redirect": "https://example.com/success",
  "failure_redirect": "https://example.com/failure"
}
```

**Wrong Amount Format**

```javascript
// Wrong - decimal amount
{ "price": 299.00 }

// Correct - cents as integer
{ "price": 29900 }
```

**Invalid Expiry Format**

```text
Wrong: 12/2028, 2028-12, 1228
Correct: 12/28 (MM/YY format)
```

**Invalid Currency**

```text
Wrong: "Rand", "rand", "R"
Correct: "ZAR" (ISO 4217)
```

## 3DS Issues [#3ds-issues]

### Issue: 3DS Challenge Not Loading [#issue-3ds-challenge-not-loading]

```mermaid
flowchart TD
    A[3DS Not Loading] --> B{Form submitted correctly?}
    B -->|No| C[Fix form submission]
    B -->|Yes| D{Correct fields included?}
    D -->|No| E[Add MD, PaReq, TermUrl]
    D -->|Yes| F{Correct target URL?}
    F -->|No| G[Use 3ds.3ds_url]
    F -->|Yes| H[Check browser console for errors]
```

**Required 3DS Form Fields:**

```html
<form method='POST' action='{{3ds_url}}'>
  <input type='hidden' name='MD' value='{{MD}}' />
  <input type='hidden' name='PaReq' value='{{PaReq}}' />
  <input type='hidden' name='TermUrl' value='{{callback_url}}' />
</form>
```

### Issue: 3DS Callback Not Received [#issue-3ds-callback-not-received]

**Causes:**

- `callback_url` not accessible from internet
- Firewall blocking incoming requests
- URL mismatch (http vs https)

**Solution:** Ensure your callback URL is publicly accessible, uses HTTPS in production, and returns HTTP 200 within 5 seconds.

### Issue: `3ds_authentication_failed` Error [#issue-3ds_authentication_failed-error]

Allow the customer to retry. If persistent with a specific card/bank, investigate with Precium support.

## Direct Post Issues [#direct-post-issues]

### Issue: "Direct Post URL Expired" [#issue-direct-post-url-expired]

**Cause:** Direct post URLs are single-use and expire after the first submission.

**Solution:** Create a new purchase to get a fresh `direct_post_url`.

### Issue: Card Data Submission Fails [#issue-card-data-submission-fails]

| Check | Correct Format |
|---|---|
| Card number | Digits only, no spaces (e.g., `4000000000001091`) |
| Expiry | MM/YY format (e.g., `12/28`) |
| CVC | 3 digits (4 for Amex) |
| Content-Type | `application/x-www-form-urlencoded` |
| API Key | Must use S2S key, not Standard key |

**Working Example:**

```bash
curl -X POST "DIRECT_POST_URL" \
  -H "Authorization: Bearer YOUR_S2S_API_KEY" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "cardholder_name=John Smith" \
  -d "card_number=4000000000001091" \
  -d "expires=12/28" \
  -d "cvc=123" \
  -d "remote_ip=196.21.45.123" \
  -d "user_agent=Mozilla/5.0" \
  -d "accept_header=text/html" \
  -d "language=en" \
  -d "java_enabled=false" \
  -d "javascript_enabled=true" \
  -d "color_depth=24" \
  -d "utc_offset=-120" \
  -d "screen_width=1920" \
  -d "screen_height=1080"
```

## Charge/Payment Issues [#chargepayment-issues]

### Issue: Charge Returns Error [#issue-charge-returns-error]

```mermaid
flowchart TD
    A[Charge Failed] --> B{Check error code}
    B --> C{Type?}
    C -->|Validation| D[Fix input data]
    C -->|Auth Failure| E[3DS not completed]
    C -->|Decline| F{Soft or Hard?}
    C -->|Network| G[Retry later]
    F -->|Soft| H[Can retry with same card]
    F -->|Hard| I[Request different card]
    E --> J[Complete 3DS first]
    D --> K[Correct and retry]
```

## Pre-Authorization Issues [#pre-authorization-issues]

### Issue: Capture Fails After Authorization [#issue-capture-fails-after-authorization]

| Error | Cause | Solution |
|---|---|---|
| `authorization_expired` | Auth held too long | Re-authorize with customer |
| `capture_amount_exceeds_authorization` | Capturing more than authed | Reduce capture amount |
| `already_captured` | Duplicate capture | Check transaction status |
| `already_voided` | Auth was cancelled | Re-authorize |

**Diagnostic Steps:**

1. Check purchase status — must be "authorized"
2. Verify capture amount ≤ authorized amount
3. Check authorization age (7-30 days typical expiry)

```python
purchase = client.get_purchase(purchase_id)
if purchase['status'] != 'authorized':
    print(f"Cannot capture - status is: {purchase['status']}")
```

### Issue: Partial Capture Not Working [#issue-partial-capture-not-working]

**Error:**`partial_capture_not_supported`

**Solution:**

- Capture the full amount and refund the difference, OR
- Void and create a new authorization for the correct amount
- Contact Precium to enable partial capture

### Issue: Pre-Authorization Void Fails [#issue-pre-authorization-void-fails]

```python
purchase = client.get_purchase(purchase_id)
if purchase['status'] == 'paid':
    # Already captured - use refund
    client.refund_purchase(purchase_id)
elif purchase['status'] == 'authorized':
    # Can void
    client.void_purchase(purchase_id)
```

## Preauthorisation Issues [#preauthorisation-issues]

### Issue: Preauthorisation Not Permitted [#issue-preauthorisation-not-permitted]

**Error:**`zero_auth_not_permitted`

**Solutions:**

1. Contact Precium to enable preauthorisation
2. Use a minimum amount (e.g., R1.00) and immediately refund
3. Use network token validation instead

### Issue: Preauthorisation Doesn't Generate Token [#issue-preauthorisation-doesnt-generate-token]

```python
purchase = client.create_zero_auth_purchase(
    client_id=customer_id,
    currency='ZAR',
    success_redirect='https://...',
    failure_redirect='https://...',
    force_recurring=True  # Required for tokenization
)
```

## External 3DS (MPI) Issues [#external-3ds-mpi-issues]

### Issue: External 3DS Data Rejected [#issue-external-3ds-data-rejected]

**Error:**`invalid_external_3ds_data`

| Field | Required | Format |
|---|---|---|
| `is_external_3DS` | Yes | `true` |
| `authentication_transaction_id` | Yes | Your MPI's transaction ID |
| `cavv` | Yes | Base64 encoded |
| `eci_raw` | Yes | "05", "02", "06", "01", etc. |
| `xid` | 3DS1 only | Base64 encoded |
| `ds_trans_id` | 3DS2 only | UUID format |

```python
# Wrong - missing is_external_3DS
payment_details = {
    "card": {
        "cavv": "AAA...",
        "eci_raw": "05"
    }
}

# Correct
payment_details = {
    "card": {
        "is_external_3DS": True,  # Required
        "authentication_transaction_id": "YOUR_MPI_TX_ID",
        "cavv": "AAA...",
        "eci_raw": "05",
        "ds_trans_id": "uuid-here"  # For 3DS2
    }
}
```

### Issue: External 3DS Brand Not Configured [#issue-external-3ds-brand-not-configured]

**Error:**`external_3ds_not_enabled`

**Cause:** Must use a Non-3DS brand for external MPI. Contact Precium to create a Non-3DS brand.

## Network Token Issues [#network-token-issues]

### Issue: Invalid Network Token [#issue-invalid-network-token]

**Error:**`invalid_network_token`

Request fresh token from MDES/VTS and verify token requestor ID is correct.

### Issue: Cryptogram Expired [#issue-cryptogram-expired]

**Error:**`cryptogram_expired`

```python
# Request fresh cryptogram immediately before transaction
cryptogram = request_new_cryptogram_from_token_service()
# Submit transaction immediately after
```

## Webhook Issues [#webhook-issues]

### Issue: Webhooks Not Arriving [#issue-webhooks-not-arriving]

```mermaid
flowchart TD
    A[Webhooks Not Arriving] --> B{URL configured in dashboard?}
    B -->|No| C[Configure webhook URL]
    B -->|Yes| D{Endpoint accessible?}
    D -->|No| E[Check firewall/DNS]
    D -->|Yes| F{SSL valid?}
    F -->|No| G[Fix SSL certificate]
    F -->|Yes| H{Returning 200?}
    H -->|No| I[Fix endpoint code]
    H -->|Yes| J[Check Precium webhook logs]
```

**Verification:**

```bash
curl -X POST https://your-webhook-url.com/precium \
  -H "Content-Type: application/json" \
  -d '{"test": true}'
```

### Issue: Webhook Signature Verification Fails [#issue-webhook-signature-verification-fails]

| Cause | Solution |
|---|---|
| Wrong secret | Copy fresh secret from dashboard |
| Parsed JSON instead of raw | Use raw request body for signature |
| Encoding issues | Ensure UTF-8 encoding throughout |
| Missing timestamp | Include timestamp in signature calculation |

**Debugging:**

```python
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding

# Callbacks are signed with asymmetric RSA, not a shared secret.
# X-Signature = base64( RSA PKCS#1 v1.5 over the SHA-256 digest of the RAW body )
# Webhook subscriptions: public_key on the webhook object.
# Per-purchase success_callback: GET /public_key/

print(f"Received X-Signature: {signature}")
print(f"Raw payload length: {len(raw_body)}")

public_key = serialization.load_pem_public_key(public_key_pem.encode())
try:
    public_key.verify(
        base64.b64decode(signature),
        raw_body,                      # raw bytes, never a re-serialised dict
        padding.PKCS1v15(),
        hashes.SHA256(),
    )
    print("Signature valid")
except Exception as exc:
    print(f"Signature INVALID: {exc}")
```

### Issue: Webhook Timeout Errors [#issue-webhook-timeout-errors]

**Solution — Async Processing:**

```python
# Wrong - synchronous processing
@app.route('/webhook', methods=['POST'])
def webhook():
    data = request.json
    process_payment(data)  # Slow - might timeout
    return '', 200

# Correct - acknowledge immediately, process async
@app.route('/webhook', methods=['POST'])
def webhook():
    if not verify_signature(request):
        return '', 401
    webhook_id = request.headers.get('X-Webhook-ID')
    queue.enqueue(process_webhook, request.json, webhook_id)
    return '', 200  # Return immediately
```

### Issue: Duplicate Webhooks [#issue-duplicate-webhooks]

```python
import redis
redis_client = redis.Redis()

def handle_webhook(data, webhook_id):
    cache_key = f"webhook:{webhook_id}"
    if redis_client.exists(cache_key):
        return  # Already processed
    redis_client.setex(cache_key, 86400, "processing")
    try:
        process_event(data)
        redis_client.setex(cache_key, 86400, "completed")
    except Exception as e:
        redis_client.delete(cache_key)
        raise
```

### Issue: Testing Webhooks Locally [#issue-testing-webhooks-locally]

```bash
# Install and start ngrok
ngrok http 3000
# Use the generated URL in Precium dashboard
# e.g., https://abc123.ngrok.io/webhooks/precium
```

**Trigger a test webhook:**

```bash
curl -X POST https://gate.reviopay.com/api/v1/webhooks/test/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "purchase.paid",
    "webhook_url": "https://abc123.ngrok.io/webhooks/precium"
  }'
```

## Payment Decline Issues [#payment-decline-issues]

### Issue: "insufficient_funds" [#issue-insufficient_funds]

- Notify the customer of insufficient funds
- Suggest retry after funding the account
- Offer alternative payment methods
- For MIT: Schedule retry per retry logic

**Customer Message:***"Your payment couldn't be processed due to insufficient funds. Please add funds to your account and try again."*

### Issue: "do_not_honour" [#issue-do_not_honour]

**Potential Causes:**

- Card restrictions
- Unusual spending pattern
- Bank's fraud detection
- Account issues

**Actions:**

1. Suggest the customer contact their bank
2. Offer an alternative payment method
3. For MIT: Retry per schedule

### Issue: Token-Based Charge Fails [#issue-token-based-charge-fails]

**Common Causes:**

- Token expired
- Original card expired/replaced
- Wrong `previous_network_transaction_id`
- Incorrect `original_amount_cents`

**Verification Checklist:**

- Token is from the same client
- `previous_network_transaction_id` matches original CIT
- `original_amount_cents` matches tokenization amount
- `is_recurring: true` in purchase

### Issue: Refund Amount Exceeds Original [#issue-refund-amount-exceeds-original]

```python
original_amount = 29900
already_refunded = 5000
max_refundable = original_amount - already_refunded

if requested_refund > max_refundable:
    raise ValueError(f"Can only refund up to {max_refundable}")
```

### Issue: Refund Pending for Days [#issue-refund-pending-for-days]

| Scenario | Timeline |
|---|---|
| Same-day (pre-settlement) | Cannot be voided — the refund runs to completion |
| Post-settlement | 5-10 business days |
| International cards | Up to 15 business days |

## Testing Issues [#testing-issues]

### Issue: Test Cards Not Working in Production [#issue-test-cards-not-working-in-production]

Test card numbers only work in the sandbox environment. Use real cards in production.

### Issue: Real Cards Not Working in Sandbox [#issue-real-cards-not-working-in-sandbox]

Sandbox doesn't process real cards. Use designated test card numbers for sandbox testing.

## Performance Issues [#performance-issues]

### Issue: API Requests Are Slow [#issue-api-requests-are-slow]

**Optimisation Checklist:**

1. Use connection pooling
2. Implement request timeouts
3. Cache client IDs (don't create new clients each time)
4. Use webhooks instead of polling for status

```python
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

session = requests.Session()
retry = Retry(total=3, backoff_factor=0.5)
adapter = HTTPAdapter(max_retries=retry, pool_connections=10, pool_maxsize=10)
session.mount('https://', adapter)
```

## When to Contact Support [#when-to-contact-support]

Contact Precium support (support@precium.com) when:

- URGENT priority errors persist
- Unexpected error codes not in documentation
- Settlement or reconciliation discrepancies
- Account configuration issues
- Suspected fraud or security concerns

**Include in Support Request:**

- Purchase ID(s)
- Timestamp of issue
- Full error response
- Steps to reproduce
- Expected vs actual behaviour

## Error Code Quick Lookup [#error-code-quick-lookup]

| Error | Action |
|---|---|
| `insufficient_funds` | Retry later, different card |
| `do_not_honour` | Retry later, contact bank |
| `expired_card` | New card required |
| `3ds_authentication_failed` | Retry authentication |
| `acquirer_configuration_error` | Contact support |
| `invalid_card_number` | Check card number format |
| `antifraud_general` | Do not retry, investigate |
| `authorization_expired` | Re-authorize |
| `capture_amount_exceeds_authorization` | Reduce capture amount |
| `void_not_permitted` | Check transaction status, use refund if settled |
| `partial_capture_not_supported` | Capture full amount or void and re-auth |
| `refund_amount_exceeds_original` | Check total refunded amount |
| `invalid_external_3ds_data` | Verify all required 3DS fields |
| `zero_auth_not_permitted` | Contact support to enable |
| `invalid_network_token` | Request new token from MDES/VTS |
