Skip to main content

Error Response Structure

All errors follow a consistent format:
{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable error message",
    "details": {
      // additional context
    }
  },
  "requestId": "req_abc123"
}

HTTP Status Codes

StatusCategoryDescription
400Client ErrorInvalid request parameters or validation error
401AuthenticationInvalid or missing API key
403AuthorizationInsufficient permissions or balance
404Not FoundResource doesn’t exist
429Rate LimitToo many requests
500Server ErrorInternal server error
503UnavailableService temporarily unavailable

Common Error Codes

MISSING_ACCESS_TOKEN

  • Message: Access token is required
  • Cause: The X-Access-Token header is missing
  • Action: Include your API key in the header
// ✅ 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

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
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
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

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
// 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
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

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

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
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');
}

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
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

Implementation Best Practices

1. Always Check Success Field

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

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

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

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

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:
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:
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

Critical: Never refund immediately on UNKNOWN_ERROR. Always wait 24 hours for resolution.
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:
// 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

Validate Early

Validate input client-side before API calls to catch errors early

Retry Smart

Use exponential backoff for 5xx errors, never retry 4xx errors

Log Context

Always include requestId and context in logs for debugging

User Feedback

Show clear, actionable error messages to users

Next Steps