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

# Error Handling

> Complete guide to handling errors effectively with the Flexy API

## Error Response Structure

All errors follow a consistent format:

```json theme={null}
{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable error message",
    "details": {
      // additional context
    }
  },
  "requestId": "req_abc123"
}
```

## HTTP Status Codes

| Status | Category       | Description                                    |
| ------ | -------------- | ---------------------------------------------- |
| `400`  | Client Error   | Invalid request parameters or validation error |
| `401`  | Authentication | Invalid or missing API key                     |
| `403`  | Authorization  | Insufficient permissions or balance            |
| `404`  | Not Found      | Resource doesn't exist                         |
| `429`  | Rate Limit     | Too many requests                              |
| `500`  | Server Error   | Internal server error                          |
| `503`  | Unavailable    | Service temporarily unavailable                |

## Common Error Codes

<AccordionGroup>
  <Accordion title="Authentication Errors (401)">
    ### MISSING\_ACCESS\_TOKEN

    * **Message**: Access token is required
    * **Cause**: The `X-Access-Token` header is missing
    * **Action**: Include your API key in the header

    ```javascript theme={null}
    // ✅ Correct
    fetch(url, {
      headers: { 'X-Access-Token': 'YOUR_API_KEY' }
    })
    ```

    ### INVALID\_ACCESS\_TOKEN / ERR\_AUTH

    * **Message**: The provided access token is invalid
    * **Cause**: API key is incorrect, expired, or revoked
    * **Action**:
      * Verify API key is correct
      * Generate a new key if needed
      * Don't log sensitive key values
      * Contact support if issue persists
  </Accordion>

  <Accordion title="Authorization Errors (403)">
    ### NO\_BALANCE / INSUFFICIENT\_BALANCE

    * **Message**: Insufficient balance
    * **Cause**: Account balance is too low
    * **Action**:
      * Check balance using `/v3/account/balance` before operations
      * Display current balance to user
      * Offer top-up option
      * Don't retry without adding funds

    ```javascript theme={null}
    const balanceRes = await fetch('https://api.oneclickdz.com/v3/account/balance', {
      headers: { 'X-Access-Token': API_KEY }
    });
    const { data } = await balanceRes.json();

    if (data.balance < requiredAmount) {
      throw new Error('Insufficient balance');
    }
    ```

    ### DUPLICATED\_REF

    * **Message**: This reference ID is already in use
    * **Cause**: Reference already used in previous request
    * **Action**:
      * Check status of existing order
      * Generate new unique reference
      * Don't create duplicate orders

    ```javascript theme={null}
    const ref = `order-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    ```

    ### IP\_BLOCKED

    * **Message**: Your IP has been temporarily blocked
    * **Cause**: Too many failed authentication attempts
    * **Action**:
      * Wait 15 minutes for automatic unblock
      * Verify correct API key
      * Contact support if persists

    ### IP\_NOT\_ALLOWED

    * **Message**: Your IP address is not whitelisted
    * **Cause**: IP whitelisting is enabled
    * **Action**: Add your IP to whitelist in dashboard
  </Accordion>

  <Accordion title="Validation Errors (400)">
    ### ERR\_VALIDATION

    * **Message**: Validation error
    * **Cause**: Request parameters don't meet requirements
    * **Common Issues**: Missing fields, invalid data types, pattern mismatch
    * **Action**:
      * Validate input client-side first
      * Check error.details for specific field issues
      * Don't retry without fixing the issue

    ```javascript theme={null}
    // Validate before sending
    function validateTopupRequest(data) {
      const errors = [];
      
      if (!data.plan_code) {
        errors.push({ field: 'plan_code', message: 'Plan code is required' });
      }
      
      if (!data.MSSIDN || !/^0[567][0-9]{8}$/.test(data.MSSIDN)) {
        errors.push({ field: 'MSSIDN', message: 'Invalid phone number format' });
      }
      
      if (isDynamicPlan(data.plan_code)) {
        if (!data.amount || data.amount < 50 || data.amount > 5000) {
          errors.push({ field: 'amount', message: 'Amount must be between 50 and 5000' });
        }
      }
      
      return errors.length > 0 ? { valid: false, errors } : { valid: true };
    }
    ```

    ### ERR\_PHONE

    * **Message**: Invalid phone number
    * **Cause**: Phone number is incorrect or doesn't exist
    * **Action**: Use `/v3/internet/check-number` to validate first

    ```javascript theme={null}
    const checkRes = await fetch(
      `https://api.oneclickdz.com/v3/internet/check-number?type=ADSL&number=${number}`,
      { headers: { 'X-Access-Token': API_KEY } }
    );

    if (!checkRes.ok) {
      throw new Error('Invalid phone number');
    }
    ```

    ### ERR\_STOCK

    * **Message**: Product out of stock
    * **Cause**: Requested product/card value is not available
    * **Action**:
      * Check stock before ordering
      * Offer alternative denominations
      * Retry later
  </Accordion>

  {" "}

  <Accordion title="Not Found Errors (404)">
    ### NOT\_FOUND - **Message**: Resource not found - **Cause**: The requested

    resource doesn't exist - **Common Cases**: Invalid order ID, invalid
    reference, deleted resource - **Action**: - Verify the ID/reference is correct

    * Check for typos - Handle gracefully in UI
  </Accordion>

  <Accordion title="Rate Limit Errors (429)">
    ### RATE\_LIMIT\_EXCEEDED

    * **Message**: Too many requests
    * **Cause**: Exceeded the rate limit
    * **Limits**: Sandbox: 60 req/min, Production: 120 req/min
    * **Action**: Implement exponential backoff with retry logic

    ```javascript theme={null}
    async function fetchWithRetry(url, options, maxRetries = 3) {
      for (let i = 0; i < maxRetries; i++) {
        const response = await fetch(url, options);
        
        if (response.status === 429) {
          const retryAfter = response.headers.get('Retry-After') || Math.pow(2, i);
          await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
          continue;
        }
        
        return response;
      }
      
      throw new Error('Max retries exceeded');
    }
    ```
  </Accordion>

  <Accordion title="Server Errors (500/503)">
    ### INTERNAL\_SERVER\_ERROR / INTERNAL\_ERROR

    * **Message**: Developer was notified and will check shortly
    * **Cause**: Unexpected error on our servers
    * **Action**:
      * **Don't refund immediately - wait 24 hours**
      * Save requestId for support
      * Implement retry logic with backoff
      * Contact support with details
      * We're automatically notified

    ```javascript theme={null}
    if (response.status === 500) {
      console.error('Server error:', data.requestId);
      // Mark for review - don't refund yet
      await scheduleStatusCheck(orderId, 24 * 60 * 60 * 1000);
    }
    ```

    ### ERR\_SERVICE

    * **Message**: Service temporarily unavailable
    * **Cause**: Service maintenance or temporary issue
    * **Action**:
      * Show maintenance message
      * Retry after delay
      * Monitor for resolution
  </Accordion>
</AccordionGroup>

## Implementation Best Practices

### 1. Always Check Success Field

```javascript theme={null}
const response = await fetch(url, options);
const data = await response.json();

if (data.success) {
  return data.data;
} else {
  throw new Error(`${data.error.code}: ${data.error.message}`);
}
```

### 2. Handle Specific Error Codes

```javascript theme={null}
async function sendTopUp(params) {
  const response = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Access-Token": API_KEY,
    },
    body: JSON.stringify(params),
  });

  const data = await response.json();

  if (!data.success) {
    switch (data.error.code) {
      case "NO_BALANCE":
      case "INSUFFICIENT_BALANCE":
        throw new Error("Insufficient balance. Please add funds.");

      case "DUPLICATED_REF":
        return await checkOrderStatus(params.ref);

      case "ERR_VALIDATION":
        throw new Error(`Invalid input: ${data.error.message}`);

      case "INTERNAL_SERVER_ERROR":
      case "INTERNAL_ERROR":
        await scheduleStatusCheck(params.ref);
        throw new Error("Temporary error, checking status later");

      default:
        throw new Error(`${data.error.code}: ${data.error.message}`);
    }
  }

  return data.data;
}
```

### 3. Smart Retry Logic with Exponential Backoff

```javascript theme={null}
async function safeApiCall(apiFunction, options = {}) {
  const { maxRetries = 3, retryDelay = 1000, retryOn = [500, 503] } = options;

  let lastError;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await apiFunction();
    } catch (error) {
      lastError = error;

      // Don't retry on client errors (4xx)
      if (error.status >= 400 && error.status < 500) {
        throw error;
      }

      // Retry on specific status codes
      if (retryOn.includes(error.status) && attempt < maxRetries) {
        const delay = retryDelay * Math.pow(2, attempt - 1);
        console.log(`Retry ${attempt}/${maxRetries} after ${delay}ms`);
        await new Promise((resolve) => setTimeout(resolve, delay));
        continue;
      }

      throw error;
    }
  }

  throw lastError;
}

// Usage
try {
  const result = await safeApiCall(() => sendMobileTopup(data), {
    maxRetries: 3,
    retryDelay: 2000,
  });
} catch (error) {
  handleError(error);
}
```

### 4. User-Friendly Error Messages

```javascript theme={null}
const errorMessages = {
  MISSING_ACCESS_TOKEN: "Authentication required. Please check your API key.",
  INVALID_ACCESS_TOKEN: "Invalid API key. Please verify your credentials.",
  ERR_AUTH: "Authentication failed. Please check your API key.",
  NO_BALANCE: "Insufficient balance. Please top up your account.",
  INSUFFICIENT_BALANCE: "Insufficient balance. Please top up your account.",
  DUPLICATED_REF: "This order was already processed.",
  ERR_VALIDATION: "Please check your input and try again.",
  ERR_PHONE: "Invalid phone number. Please verify and try again.",
  ERR_STOCK: "This product is currently out of stock.",
  NOT_FOUND: "The requested resource was not found.",
  RATE_LIMIT_EXCEEDED: "Too many requests. Please wait a moment and try again.",
  ERR_SERVICE: "Service temporarily unavailable. Please try again.",
  INTERNAL_SERVER_ERROR: "An error occurred. Our team has been notified.",
  INTERNAL_ERROR: "An error occurred. Our team has been notified.",
};

function getUserFriendlyMessage(errorCode, defaultMessage) {
  return (
    errorMessages[errorCode] ||
    defaultMessage ||
    "An unexpected error occurred."
  );
}

// Usage
if (!data.success) {
  const userMessage = getUserFriendlyMessage(
    data.error.code,
    data.error.message
  );
  showErrorToUser(userMessage);
}
```

### 5. Log Request IDs

```javascript theme={null}
function logError(error, context) {
  const errorLog = {
    timestamp: new Date().toISOString(),
    requestId: error.requestId,
    code: error.code,
    message: error.message,
    context: {
      userId: context.userId,
      operation: context.operation,
      // Don't log sensitive data
    },
  };

  // Log to your logging service
  logger.error("API Error", errorLog);

  // Alert on critical errors
  if (error.status >= 500) {
    alertOps("API Error", errorLog);
  }
}
```

## Advanced Patterns

### Circuit Breaker Pattern

Prevent cascading failures by stopping requests when error rate is high:

```javascript theme={null}
class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.failureCount = 0;
    this.threshold = threshold;
    this.timeout = timeout;
    this.state = "CLOSED"; // CLOSED, OPEN, HALF_OPEN
    this.nextAttempt = Date.now();
  }

  async execute(fn) {
    if (this.state === "OPEN") {
      if (Date.now() < this.nextAttempt) {
        throw new Error("Circuit breaker is OPEN");
      }
      this.state = "HALF_OPEN";
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failureCount = 0;
    this.state = "CLOSED";
  }

  onFailure() {
    this.failureCount++;
    if (this.failureCount >= this.threshold) {
      this.state = "OPEN";
      this.nextAttempt = Date.now() + this.timeout;
    }
  }
}

// Usage
const breaker = new CircuitBreaker(5, 60000);

try {
  const result = await breaker.execute(() => sendTopup(data));
} catch (error) {
  if (error.message === "Circuit breaker is OPEN") {
    showMaintenanceMessage();
  }
}
```

### Error Monitoring

Track error patterns to identify issues early:

```javascript theme={null}
class ApiClient {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.errorStats = new Map();
  }

  trackError(errorCode) {
    const count = this.errorStats.get(errorCode) || 0;
    this.errorStats.set(errorCode, count + 1);
  }

  async request(url, options) {
    try {
      const response = await fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          "X-Access-Token": this.apiKey,
        },
      });

      const data = await response.json();

      if (!data.success) {
        this.trackError(data.error.code);

        // Alert if too many errors
        if (this.errorStats.get(data.error.code) > 10) {
          this.sendAlert(`High error rate: ${data.error.code}`);
        }
      }

      return data;
    } catch (error) {
      this.trackError("NETWORK_ERROR");
      throw error;
    }
  }

  sendAlert(message) {
    console.error("ALERT:", message);
    // Send to your monitoring service
  }
}
```

### Handling UNKNOWN\_ERROR

<Warning>
  **Critical:** Never refund immediately on UNKNOWN\_ERROR. Always wait 24 hours
  for resolution.
</Warning>

```javascript theme={null}
async function handleUnknownError(orderId, topupId) {
  // Mark for review
  await db.orders.update({
    where: { id: orderId },
    data: {
      status: "REVIEW_NEEDED",
      reviewReason: "UNKNOWN_ERROR from API",
      reviewScheduled: new Date(Date.now() + 24 * 60 * 60 * 1000),
    },
  });

  // Show message to user
  await notifyUser(orderId, {
    title: "Order Under Review",
    message:
      "Your order is being verified. Status will update within 24 hours.",
  });

  // Schedule automatic recheck
  await scheduleJob("recheck-unknown-error", {
    orderId,
    topupId,
    runAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
  });
}
```

## Testing Error Scenarios

Use sandbox mode to test error handling:

```javascript theme={null}
// Test insufficient balance
// Set your sandbox balance very low

// Test validation errors
await sendTopUp({
  plan_code: 'INVALID_PLAN',
  MSSIDN: '123456789',     // Invalid format
  amount: -100             // Negative amount
});

// Test duplicate reference
const ref = 'test-duplicate-123';
await sendTopUp({ ref, ... });  // First request
await sendTopUp({ ref, ... });  // Should fail with DUPLICATED_REF
```

## Quick Reference

<CardGroup cols={2}>
  <Card title="Validate Early" icon="circle-check">
    Validate input client-side before API calls to catch errors early
  </Card>

  <Card title="Retry Smart" icon="arrows-rotate">
    Use exponential backoff for 5xx errors, never retry 4xx errors
  </Card>

  <Card title="Log Context" icon="file-lines">
    Always include requestId and context in logs for debugging
  </Card>

  <Card title="User Feedback" icon="message">
    Show clear, actionable error messages to users
  </Card>
</CardGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Core Endpoints" icon="code" href="/en/api-reference/core/health-check">
    Explore all endpoints
  </Card>

  <Card title="Response Format" icon="file-code" href="/en/api-reference/response-format">
    Understanding API responses
  </Card>
</CardGroup>
