# Error Handling

Understanding and handling errors in the AuxCORE API.

---

## Overview

The AuxCORE API uses conventional HTTP response codes to indicate the success or failure of an API request. Errors include helpful information to help you diagnose and resolve issues.

---

## HTTP Status Codes

| Code | Status | Meaning |
|------|--------|---------|
| 200 | OK | Request succeeded (also: a payment accepted but not yet settled, or one that needs a 3DS / hosted redirect) |
| 201 | Created | Resource created successfully |
| 400 | Bad Request | Invalid request parameters (`VALIDATION_ERROR`, `PCI_SCOPE_VIOLATION`, …) |
| 401 | Unauthorized | Missing or invalid API key or token |
| 402 | Payment Required | `POST /api/v1/public/transaction` was declined |
| 403 | Forbidden | Insufficient permissions, an SDK key used where it is not allowed, or a service not in the tenant's subscription (`SERVICE_UNAVAILABLE`) |
| 404 | Not Found | Resource or tenant doesn't exist, or the feature is turned off for the tenant/merchant |
| 409 | Conflict | Resource conflict, or an idempotent request still in progress |
| 422 | Unprocessable Entity | Idempotency key reused with a different body |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server error, or a downstream service unreachable |
| 502 | Bad Gateway | Upstream processor or service error |
| 503 | Service Unavailable | A downstream service is temporarily down (`SERVICE_UNAVAILABLE`) |
| 504 | Gateway Timeout | A downstream service timed out (`GATEWAY_TIMEOUT`) |

---

## Error Response Structure

Services return errors in this format:

```json
{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable error message"
  }
}
```

### Fields

| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | Always `false` for errors |
| `error.code` | string | Machine-readable error code |
| `error.message` | string | Human-readable description. Validation messages name the offending field |

Two Public API exceptions to plan for:

- **API key failures** on `/api/v1/public/*` return `error` as a **string** plus a `message`, e.g. `{ "success": false, "error": "API key validation failed", "message": "..." }`. See [Authentication](./authentication.md).
- **Payment results** from `POST /api/v1/public/transaction` use a legacy envelope: `{ "success", "status": "success" | "pending" | "declined", "status_code", "message", "data": { "Status", "ResponseCode", ... } }`.

Branch on `error.code` when `error` is an object, and on the HTTP status otherwise.

---

## Common Error Codes

### Authentication Errors

#### UNAUTHORIZED
```json
{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Authentication required. Provide a Bearer token, auth cookie, or x-api-key header."
  }
}
```
**Solution:** Send a valid JWT or API key. Also returned for wrong login credentials (`Invalid credentials`).

#### TOKEN_EXPIRED
```json
{
  "success": false,
  "error": {
    "code": "TOKEN_EXPIRED",
    "message": "Access token has expired. Please refresh your token."
  }
}
```
**Solution:** Call `POST /api/v1/public/refreshToken` (or `POST /api/v1/auth/refresh`) and retry with the new token.

#### INVALID_TOKEN / INVALID_API_KEY
Returned with `401` for a malformed or forged JWT (`INVALID_TOKEN`) or an unknown/deactivated API key on an Admin API route (`INVALID_API_KEY`).
**Solution:** Re-authenticate, or check the key on `/settings/developer/api-keys`.

#### TENANT_REQUIRED
```json
{
  "success": false,
  "error": {
    "code": "TENANT_REQUIRED",
    "message": "Tenant identification required. Provide X-Tenant-ID header or authenticate with a valid JWT."
  }
}
```
**Solution:** Sent with `400` when a request carries neither a JWT, an API key, nor a resolvable tenant. Include `X-Tenant-ID`, or authenticate — API-key calls resolve the tenant from the key.

#### TENANT_NOT_FOUND
```json
{
  "success": false,
  "error": {
    "code": "TENANT_NOT_FOUND",
    "message": "Tenant acme-prod not found"
  }
}
```
**Solution:** Verify the `X-Tenant-ID` you send (`404`). With an API key you can omit the header entirely.

---

### Validation Errors

#### VALIDATION_ERROR
```json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Email and password are required"
  }
}
```
**Solution:** Fix the request and retry (`400`).

#### PCI_SCOPE_VIOLATION
```json
{
  "success": false,
  "error": {
    "code": "PCI_SCOPE_VIOLATION",
    "message": "PublicAPI.transaction: raw card data is not permitted in this environment. Collect card data via the PCI iframe/SDK and submit a pciToken only."
  }
}
```
**Solution:** Send a `pciToken` (`pci_tok_…`) from the PCI SDK or `POST /api/v1/public/tokenize-card` instead of `CardNumber` / `CVV`.

---

### Resource Errors

#### NOT_FOUND
```json
{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Transaction with id 550e8400-e29b-41d4-a716-446655440020 not found"
  }
}
```
**Solution:** Verify the resource ID exists and belongs to your merchant.

#### CONFLICT
```json
{
  "success": false,
  "error": {
    "code": "CONFLICT",
    "message": "Transaction already voided"
  }
}
```
**Solution:** `409` — check the resource state before retrying.

---

### Payment Errors

#### Declined payment
A decline from `POST /api/v1/public/transaction` is HTTP **402**:

```json
{
  "success": false,
  "status": "declined",
  "status_code": 402,
  "message": "Insufficient funds",
  "data": {
    "TransactionId": "550e8400-e29b-41d4-a716-446655440021",
    "Status": "9",
    "Amount": 50.00,
    "ResponseCode": "202",
    "ResponseMessage": "Insufficient funds",
    "AvsResult": "N",
    "CvvResult": "M"
  }
}
```

`ResponseCode` uses the NMI convention: `100` approved, `2xx` issuer declines (e.g. `201` do not honor, `202` insufficient funds, `223` expired card, `225` invalid CVV), `300` rejected by the gateway, `400` processor error. Do not retry a decline with the same card.

A `200` with `"status": "pending"` is **not** a failure: either the payment was accepted and awaits settlement (ACH), or `requires_action` is `true` and the payer must be sent to `data.RedirectUrl`.

#### GATEWAY_ERROR
```json
{
  "success": false,
  "error": {
    "code": "GATEWAY_ERROR",
    "message": "CoinVoyage did not return a pay order"
  }
}
```
**Solution:** Returned with `502` when an upstream processor fails. Retry with exponential backoff.

---

### Rate Limiting

#### RATE_LIMITED
```json
{
  "success": false,
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests from this IP. Try again later."
  }
}
```
**Solution:** `429` — wait for the number of seconds in the `Retry-After` header. The general API limiter answers `429` with a plain-text body (`Too many requests, please try again later`), so check the status code, not just the JSON. See [Rate Limits](./rate-limits.md).

---

### Permission Errors

#### FORBIDDEN
```json
{
  "success": false,
  "error": {
    "code": "FORBIDDEN",
    "message": "API key is not authorized for the requested merchant."
  }
}
```
**Solution:** `403` — a merchant-scoped key named another merchant, an SDK key was used on an Admin API route, or the user lacks the permission.

---

### Idempotency Errors

| Code | Status | Meaning |
|------|--------|---------|
| `INVALID_IDEMPOTENCY_KEY` | 400 | Key shorter than 16 or longer than 255 characters |
| `IDEMPOTENCY_KEY_REQUIRED` | 400 | Hosted-checkout / WooCommerce payment sent without a key |
| `IDEMPOTENCY_KEY_REUSED` | 422 | Same key, different request body |
| `IDEMPOTENCY_REQUEST_IN_PROGRESS` | 409 | The first request with this key has not finished yet |
| `IDEMPOTENCY_UNAVAILABLE` | 503 | Hosted-checkout payment: the idempotency store is unreachable, so the payment is refused — retry |

---

## Handling Errors in Code

### JavaScript/Node.js

```javascript
const response = await fetch('https://app.auxcore.net/api/v1/public/transaction', {
  method: 'POST',
  headers: {
    'x-api-key': apiKey,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(transactionData)
});

if (response.status === 429) {
  const retryAfter = Number(response.headers.get('retry-after') || 60);
  await sleep(retryAfter * 1000);
  return retryRequest();
}

const result = await response.json();

if (response.status === 402 || result.status === 'declined') {
  // Card declined — show result.message to the payer, do not retry
  return { declined: true, code: result.data?.ResponseCode, message: result.message };
}

if (!response.ok) {
  // error is an object on most routes, a string on API-key failures
  const code = typeof result.error === 'string' ? result.error : result.error?.code;
  const message = result.message || result.error?.message || 'An error occurred';

  switch (code) {
    case 'TOKEN_EXPIRED':
      await refreshToken();
      return retryRequest();
    case 'VALIDATION_ERROR':
    case 'PCI_SCOPE_VIOLATION':
      console.error(`Fix the request: ${message}`);
      break;
    case 'GATEWAY_ERROR':
      return retryWithBackoff();
    default:
      console.error('API Error:', code, message);
  }
  throw new Error(message);
}

if (result.status === 'pending' && result.requires_action) {
  window.location.href = result.data.RedirectUrl;
}

return result.data;
```

### Python

```python
import requests
import time

def process_payment(transaction_data, api_key):
    url = 'https://app.auxcore.net/api/v1/public/transaction'
    headers = {
        'x-api-key': api_key,
        'Content-Type': 'application/json'
    }

    response = requests.post(url, json=transaction_data, headers=headers)

    if response.status_code == 429:
        time.sleep(int(response.headers.get('Retry-After', 60)))
        return process_payment(transaction_data, api_key)

    result = response.json()

    if response.status_code == 402 or result.get('status') == 'declined':
        print(f"Transaction declined: {result.get('message')}")
        return None

    if not response.ok:
        error = result.get('error')
        code = error if isinstance(error, str) else (error or {}).get('code', 'UNKNOWN_ERROR')
        message = result.get('message') or (error or {}).get('message', 'An error occurred')
        if code == 'GATEWAY_ERROR':
            return retry_with_backoff(transaction_data, api_key)
        raise Exception(f"{code}: {message}")

    return result.get('data')
```

---

## Retry Logic

### When to Retry

✅ **DO retry these errors:**
- `GATEWAY_ERROR` - Upstream processor failure
- `RATE_LIMITED` / any `429` - Wait for `Retry-After`
- `500` / `503` - Service temporarily down
- Network timeouts — with an `Idempotency-Key` where supported

❌ **DON'T retry these errors:**
- `VALIDATION_ERROR` / `PCI_SCOPE_VIOLATION` - Fix the request first
- `UNAUTHORIZED` / `TOKEN_EXPIRED` - Re-authenticate or refresh first
- `NOT_FOUND` - Resource doesn't exist
- `FORBIDDEN` - Insufficient permissions
- Declined transactions (`402`) - Don't retry declined cards

### Exponential Backoff

```javascript
async function retryWithBackoff(fn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      
      // Don't retry non-retryable errors
      if (!isRetryable(error)) throw error;
      
      // Wait with exponential backoff
      const delay = Math.min(1000 * Math.pow(2, i), 10000); // Max 10s
      await sleep(delay);
    }
  }
}

function isRetryable(error) {
  const retryableCodes = [
    'GATEWAY_ERROR',
    'SERVICE_UNAVAILABLE',
    'RATE_LIMITED'
  ];
  return retryableCodes.includes(error.code) || error.status >= 500;
}
```

---

## Idempotency

The Admin API payment routes — `POST /api/v1/transactions/sale`, `/auth`, `/token-sale`, `/ach-sale` and `/crypto-sale` — accept an `Idempotency-Key` header (16–255 characters). A repeat with the same key and body within 24 hours returns the stored response instead of charging again.

```bash
curl -X POST https://app.auxcore.net/api/v1/transactions/sale \
  -H "Authorization: Bearer TOKEN" \
  -H "Idempotency-Key: 6f1c2e9a-4b7d-4e2a-9c1f-0a8b3d5e7f21" \
  -H "Content-Type: application/json" \
  -d '{"merchantId":"550e8400-e29b-41d4-a716-446655440000","amount":100.00,"paymentMethod":"card","pciToken":"pci_tok_..."}'
```

> **Note:** `POST /api/v1/public/transaction` does not currently pass `Idempotency-Key` through, so the header has no effect on the Public API. Look up the transaction (`GET /api/v1/public/transaction-list`) before retrying a Public API sale after a timeout.

---

## Logging & Monitoring

### What to Log

✅ **DO log:**
- Request IDs for tracking
- Error codes and messages
- User actions that caused errors
- Timestamps
- Retry attempts

❌ **DON'T log:**
- Full card numbers
- CVV codes
- Raw API tokens
- Customer passwords

### Example Log Entry

```json
{
  "timestamp": "2026-01-28T12:34:56Z",
  "level": "error",
  "requestId": "req_abc123",
  "errorCode": "VALIDATION_ERROR",
  "errorMessage": "Invalid card number",
  "field": "card.number",
  "userId": "user_123",
  "action": "process_payment",
  "retryCount": 0
}
```

---

## Testing Errors

### Trigger Specific Errors

| Input | Result |
|-------|--------|
| `pciToken` for card `4000000000000002` (Sandbox) | Declined — HTTP 402 (may vary by processor) |
| No `x-api-key` header | 401 `API key required` |
| Raw `CardNumber` where raw card data is not allowed | 400 `PCI_SCOPE_VIOLATION` |
| Expired JWT on an Admin API route | 401 `TOKEN_EXPIRED` |

Specific decline reasons (expired card, invalid CVV, fraud) depend on the sandbox gateway configured for the merchant.

---

## Best Practices

### ✅ DO:
- **Handle all error cases** gracefully
- **Show user-friendly messages** to customers
- **Log errors** for debugging
- **Implement retry logic** for transient errors
- **Use idempotency keys** for critical operations
- **Monitor error rates** to detect issues early
- **Validate data** before sending to API

### ❌ DON'T:
- **Don't expose raw errors** to end users
- **Don't retry indefinitely** - set max attempts
- **Don't ignore errors** - always handle them
- **Don't retry validation errors** without fixing them
- **Don't log sensitive data**

---

## Complete Error Code Reference

[View all error codes →](../reference/error-codes.md)

---

## Next Steps

- [View all API endpoints →](../reference/endpoints.md)
- [Learn about rate limits →](./rate-limits.md)
- [Set up webhooks →](./webhooks.md)

---

**Need help?** Contact support@auxcore.net
