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

# Error Handling

> Handle errors gracefully in your integration

All API errors follow a consistent format.

## Error Response Format

```json theme={null}
{
  "error": "Error Type",
  "message": "Human-readable description",
  "code": "ERROR_CODE"
}
```

| Field     | Type   | Description                                                                    |
| --------- | ------ | ------------------------------------------------------------------------------ |
| `error`   | string | Short error category (e.g., `"Bad Request"`, `"Conflict"`)                     |
| `message` | string | Human-readable description of what went wrong                                  |
| `code`    | string | Machine-readable error code for programmatic handling (e.g., `KYC_INCOMPLETE`) |

<Note>
  Some error responses may include additional context fields depending on the endpoint — for example, `attemptsRemaining` on OTP errors or `matchScore` on name mismatch errors.
</Note>

## HTTP Status Codes

| Code  | Meaning                                |
| ----- | -------------------------------------- |
| `400` | Bad Request - Invalid parameters       |
| `401` | Unauthorized - Invalid/missing API key |
| `403` | Forbidden - IP not whitelisted         |
| `404` | Not Found - Resource doesn't exist     |
| `409` | Conflict - Resource already exists     |
| `422` | Unprocessable - Validation failed      |
| `429` | Too Many Requests - Rate limited       |
| `500` | Internal Error - Our fault             |

## Common Error Codes

### User Errors

| Code              | Description                      |
| ----------------- | -------------------------------- |
| `USER_EXISTS`     | Mobile number already registered |
| `USER_NOT_FOUND`  | User ID doesn't exist            |
| `KYC_INCOMPLETE`  | User hasn't completed KYC        |
| `NO_BANK_ACCOUNT` | No verified bank account         |

### Transaction Errors

| Code                         | Description                    |
| ---------------------------- | ------------------------------ |
| `PENDING_TRANSACTION_EXISTS` | User has an active transaction |
| `AMOUNT_TOO_LOW`             | Below minimum (\$10)           |
| `AMOUNT_TOO_HIGH`            | Above maximum (\$50,000)       |
| `QUOTE_EXPIRED`              | Transaction expired            |

### Payout Errors

| Code                     | Description                                    |
| ------------------------ | ---------------------------------------------- |
| `KYC_NOT_VERIFIED`       | User KYC status is not verified at payout time |
| `KYC_LEVEL_INSUFFICIENT` | User must complete at least basic KYC          |
| `BANK_ACCOUNT_INVALID`   | Bank account not found or not verified         |
| `PAYOUT_FAILED`          | Payout provider rejected the transfer          |

### KYC Errors

| Code                       | Description                |
| -------------------------- | -------------------------- |
| `PAN_INVALID`              | Invalid PAN format         |
| `PAN_NOT_FOUND`            | PAN not in database        |
| `AADHAAR_OTP_EXPIRED`      | OTP session expired        |
| `BANK_VERIFICATION_FAILED` | Penny drop failed          |
| `NAME_MISMATCH`            | Document names don't match |

## Rate Limiting

Rate-limited responses (`429`) include headers to help you manage request pacing:

| Header                  | Description                              |
| ----------------------- | ---------------------------------------- |
| `X-RateLimit-Limit`     | Maximum requests allowed per minute      |
| `X-RateLimit-Remaining` | Requests remaining in the current window |
| `X-RateLimit-Reset`     | Unix timestamp when the window resets    |

```json theme={null}
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1750003260

{
  "error": "Rate Limit Exceeded",
  "message": "Too many requests. Please try again later.",
  "retryAfter": 60
}
```

Use the `retryAfter` field (seconds) or the `X-RateLimit-Reset` header to schedule your next request.

## Error Handling Examples

<Tabs>
  <Tab title="JavaScript">
    ```javascript theme={null}
    async function createTransaction(userId, amount) {
      try {
        const response = await fetch('https://api.stablepay.global/v2/transactions', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${apiKey}`,
            'Content-Type': 'application/json',
            'Idempotency-Key': crypto.randomUUID()
          },
          body: JSON.stringify({ userId, amount, asset: 'USDC', network: 'polygon' })
        });

        if (!response.ok) {
          const error = await response.json();

          switch (error.code) {
            case 'KYC_INCOMPLETE':
              return { error: 'Please complete KYC first' };
            case 'PENDING_TRANSACTION_EXISTS':
              return { error: 'You have an active transaction' };
            case 'AMOUNT_TOO_LOW':
              return { error: 'Minimum amount is $10' };
            default:
              return { error: error.message };
          }
        }

        return await response.json();
      } catch (err) {
        return { error: 'Network error. Please try again.' };
      }
    }
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    interface StablePayError {
      error: string;
      message: string;
      code: string;
    }

    interface StablePayResponse<T> {
      success: boolean;
      data: T;
    }

    async function createTransaction(
      userId: string,
      amount: string
    ): Promise<StablePayResponse<any> | { error: string }> {
      const response = await fetch('https://api.stablepay.global/v2/transactions', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': crypto.randomUUID()
        },
        body: JSON.stringify({
          userId,
          amount,
          asset: 'USDC',
          network: 'polygon'
        })
      });

      if (!response.ok) {
        const err: StablePayError = await response.json();

        if (response.status === 429) {
          const retryAfter = parseInt(response.headers.get('retryAfter') ?? '60');
          return { error: `Rate limited. Retry in ${retryAfter}s` };
        }

        return { error: err.message };
      }

      return response.json();
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import requests
    import uuid

    def create_transaction(user_id: str, amount: str) -> dict:
        response = requests.post(
            "https://api.stablepay.global/v2/transactions",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
                "Idempotency-Key": str(uuid.uuid4()),
            },
            json={
                "userId": user_id,
                "amount": amount,
                "asset": "USDC",
                "network": "polygon",
            },
        )

        if response.status_code == 429:
            retry_after = response.json().get("retryAfter", 60)
            return {"error": f"Rate limited. Retry in {retry_after}s"}

        if not response.ok:
            err = response.json()
            return {"error": err.get("message", "Unknown error")}

        return response.json()
    ```
  </Tab>
</Tabs>

## Idempotency

For safe retries, include an `Idempotency-Key` header (UUID) on mutating requests:

```bash theme={null}
curl -X POST https://api.stablepay.global/v2/transactions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{"userId": "usr_abc", "amount": "100", "asset": "USDC", "network": "polygon"}'
```

Same idempotency key = same response (no duplicate transactions).

### Idempotency Conflict (409)

If you send the same idempotency key with a **different request body**, you'll receive a `409 Conflict`:

```json theme={null}
{
  "error": "Idempotency Conflict",
  "message": "This idempotency key was already used with a different request body",
  "code": "IDEMPOTENCY_CONFLICT"
}
```

| Scenario                    | Response                                                    |
| --------------------------- | ----------------------------------------------------------- |
| New unique key              | `201` — Transaction created                                 |
| Same key, same body (retry) | `200` — Cached response, `Idempotent-Replayed: true` header |
| Same key, different body    | `409` — Conflict, no action taken                           |

Keys expire after 24 hours.
