Skip to main content
Pro Plan15 minutesIntermediate

API Error Handling

Handle Zenovay API errors gracefully - error codes, retry strategies, and debugging techniques. Learn about api in this API integrations guide.

apierrorsdebuggingtroubleshootingbest-practices
Last updated:

Learn how to handle Zenovay API errors gracefully with proper error codes, retry strategies, and debugging techniques.

The Zenovay REST API is available on Pro, Scale, and Enterprise plans. Requests authenticated with a Free-plan key are rejected with a 403 and the code API_PAID_PLAN_REQUIRED.

Error Response Format

Every API response is wrapped in a success flag. Errors return success: false with an error object:

{
  "success": false,
  "error": {
    "message": "Rate limit exceeded (30 requests/minute). Try again in 42 seconds",
    "code": "RATE_LIMIT_EXCEEDED",
    "timestamp": "2026-06-13T10:30:00.000Z"
  }
}

The HTTP status code is on the response itself (e.g. 429), not inside the body.

Response Fields

FieldDescription
successfalse on any error, true on success
error.codeMachine-readable error code (uppercase, e.g. RATE_LIMIT_EXCEEDED)
error.messageHuman-readable error description
error.timestampISO 8601 timestamp of when the error was generated

Every response also carries an x-request-id header (a UUID). Include it when contacting support — it lets us trace a single request end-to-end.

HTTP Status Codes

Success Codes (2xx)

CodeDescription
200Request successful

Successful responses use the success: true wrapper:

{
  "success": true,
  "data": { /* ... */ },
  "timestamp": "2026-06-13T10:30:00.000Z"
}

Client Errors (4xx)

CodeError CodeDescription
400VALIDATION_ERROR / MISSING_SITE_ID / GENERIC_ERRORInvalid or missing request parameter
401UNAUTHORIZEDInvalid or missing API key
403FORBIDDENAPI key lacks access to the requested resource
403API_PAID_PLAN_REQUIREDThe API requires a paid plan (Free keys are blocked)
404NOT_FOUNDResource not found
409CONFLICTResource conflict (duplicate)
429RATE_LIMIT_EXCEEDEDToo many requests this minute
429MONTHLY_LIMIT_EXCEEDEDMonthly request quota reached

Server Errors (5xx)

CodeError CodeDescription
500INTERNAL_ERROR / GENERIC_ERRORServer error

Common Error Codes

Authentication Errors

A missing key, a malformed key (Zenovay keys start with zv_), or a key that has been revoked all return 401 with the UNAUTHORIZED code. The message tells you which one:

// Missing API key
{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Missing API key. Use Authorization: Bearer <key> or X-API-Key header",
    "timestamp": "2026-06-13T10:30:00.000Z"
  }
}

// Invalid or revoked key
{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid API key",
    "timestamp": "2026-06-13T10:30:00.000Z"
  }
}

Authenticate with either header:

Authorization: Bearer zv_YOUR_API_KEY
X-API-Key: zv_YOUR_API_KEY

Validation Errors

Missing or malformed parameters return a 400 with a plain-text message describing the problem:

// Missing required query parameter
{
  "success": false,
  "error": {
    "code": "MISSING_SITE_ID",
    "message": "site_id parameter is required",
    "timestamp": "2026-06-13T10:30:00.000Z"
  }
}

// Invalid input
{
  "success": false,
  "error": {
    "code": "GENERIC_ERROR",
    "message": "Query must be 500 characters or less",
    "timestamp": "2026-06-13T10:30:00.000Z"
  }
}

Rate Limit Errors

When you exceed the per-minute rate limit, you get a 429 with RATE_LIMIT_EXCEEDED. Read the Retry-After response header (seconds) to know how long to wait:

{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded (30 requests/minute). Try again in 45 seconds",
    "timestamp": "2026-06-13T10:30:00.000Z"
  }
}

The response includes these headers:

HeaderDescription
Retry-AfterSeconds to wait before retrying
X-RateLimit-LimitYour per-minute request limit
X-RateLimit-ResetISO 8601 timestamp when the window resets

Exhausting your monthly quota returns 429 with MONTHLY_LIMIT_EXCEEDED and X-Usage-* headers (X-Usage-Monthly, X-Usage-Limit, X-Usage-Reset).

Resource Errors

Requesting a website you don't have access to, or one that doesn't exist, returns 404 NOT_FOUND or 403 FORBIDDEN:

// Website not found
{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Website not found",
    "timestamp": "2026-06-13T10:30:00.000Z"
  }
}

// Key has no access to this website
{
  "success": false,
  "error": {
    "code": "FORBIDDEN",
    "message": "API key does not have access to this website",
    "timestamp": "2026-06-13T10:30:00.000Z"
  }
}

Plan Limit Errors

Some endpoints require a higher plan than your current one. Free keys are blocked entirely; Pro keys are blocked from Scale-only endpoints (for example the natural-language query API):

// Free key calling any API endpoint
{
  "success": false,
  "error": {
    "code": "API_PAID_PLAN_REQUIRED",
    "message": "The Zenovay API requires a paid plan. Upgrade to Pro or higher to use API keys.",
    "timestamp": "2026-06-13T10:30:00.000Z"
  }
}

// Pro key calling a Scale-only endpoint
{
  "success": false,
  "error": {
    "code": "FORBIDDEN",
    "message": "This endpoint requires a Scale plan or higher. Your current plan: Pro",
    "timestamp": "2026-06-13T10:30:00.000Z"
  }
}

Error Handling Strategies

Basic Error Handling

async function callZenovayAPI(endpoint) {
  try {
    const response = await fetch(`https://api.zenovay.com/api/external/v1${endpoint}`, {
      headers: {
        'X-API-Key': API_KEY,
      }
    });

    const body = await response.json();

    if (!response.ok || body.success === false) {
      throw new ZenovayAPIError(body.error, response.status, response.headers.get('x-request-id'));
    }

    return body.data;
  } catch (error) {
    if (error instanceof ZenovayAPIError) {
      handleAPIError(error);
    } else {
      // Network error
      console.error('Network error:', error);
    }
    throw error;
  }
}

class ZenovayAPIError extends Error {
  constructor(error, status, requestId) {
    super(error?.message);
    this.code = error?.code;
    this.status = status;
    this.requestId = requestId;
  }
}

function handleAPIError(error) {
  switch (error.code) {
    case 'UNAUTHORIZED':
      console.error('Invalid or missing API key');
      break;
    case 'API_PAID_PLAN_REQUIRED':
      console.error('This API requires a paid plan');
      break;
    case 'RATE_LIMIT_EXCEEDED':
      console.error('Rate limited - back off and retry');
      break;
    default:
      console.error(`API error: ${error.message}`);
  }
}

Retry with Exponential Backoff

Retry 429 and 5xx responses. For 429, honor the Retry-After header instead of guessing:

async function fetchWithRetry(url, options, maxRetries = 3) {
  const retryableStatus = [429, 500, 502, 503, 504];

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);
      const body = await response.json();

      if (response.ok && body.success !== false) {
        return body.data;
      }

      // Don't retry non-retryable client errors
      if (!retryableStatus.includes(response.status) || attempt === maxRetries) {
        throw new ZenovayAPIError(body.error, response.status, response.headers.get('x-request-id'));
      }

      // Prefer the server's Retry-After (seconds) on 429
      let delay;
      const retryAfter = response.headers.get('Retry-After');
      if (response.status === 429 && retryAfter) {
        delay = parseInt(retryAfter, 10) * 1000;
      } else {
        delay = Math.min(1000 * Math.pow(2, attempt), 30000); // Max 30s
      }

      console.log(`Retry attempt ${attempt + 1} after ${delay}ms`);
      await sleep(delay);

    } catch (error) {
      if (error instanceof ZenovayAPIError) {
        throw error;
      }

      // Network error - retry
      if (attempt === maxRetries) {
        throw error;
      }

      await sleep(1000 * Math.pow(2, attempt));
    }
  }
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

class ZenovayAPIError extends Error {
  constructor(error, status, requestId) {
    super(error?.message);
    this.code = error?.code;
    this.status = status;
    this.requestId = requestId;
  }
}

Rate Limit Handling

Pace your requests using the rate-limit headers so you back off before hitting a 429:

class RateLimitedClient {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.queue = [];
    this.processing = false;
    this.rateLimitReset = null;
  }

  async request(endpoint) {
    return new Promise((resolve, reject) => {
      this.queue.push({ endpoint, resolve, reject });
      this.processQueue();
    });
  }

  async processQueue() {
    if (this.processing || this.queue.length === 0) return;

    // Wait if rate limited
    if (this.rateLimitReset && Date.now() < this.rateLimitReset) {
      const waitTime = this.rateLimitReset - Date.now();
      setTimeout(() => this.processQueue(), waitTime);
      return;
    }

    this.processing = true;
    const { endpoint, resolve, reject } = this.queue.shift();

    try {
      const response = await fetch(`https://api.zenovay.com/api/external/v1${endpoint}`, {
        headers: {
          'X-API-Key': this.apiKey,
        }
      });

      // Check rate limit headers
      const remaining = response.headers.get('X-RateLimit-Remaining');
      const reset = response.headers.get('X-RateLimit-Reset');

      if (remaining === '0' && reset) {
        this.rateLimitReset = new Date(reset).getTime();
      }

      if (response.status === 429) {
        const retryAfter = response.headers.get('Retry-After');
        this.rateLimitReset = Date.now() + ((parseInt(retryAfter, 10) || 60) * 1000);

        // Re-queue the request
        this.queue.unshift({ endpoint, resolve, reject });
      } else {
        const body = await response.json();
        if (response.ok && body.success !== false) {
          resolve(body.data);
        } else {
          reject(body.error);
        }
      }

    } catch (error) {
      reject(error);
    } finally {
      this.processing = false;
      if (this.queue.length > 0) {
        setImmediate(() => this.processQueue());
      }
    }
  }
}

Circuit Breaker Pattern

class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.resetTimeout = options.resetTimeout || 30000;
    this.failures = 0;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.nextAttempt = null;
  }

  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.failures = 0;
    this.state = 'CLOSED';
  }

  onFailure() {
    this.failures++;
    if (this.failures >= this.failureThreshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.resetTimeout;
    }
  }
}

// Usage
const breaker = new CircuitBreaker();

async function getAnalytics(websiteId) {
  return breaker.execute(() =>
    fetch(`https://api.zenovay.com/api/external/v1/analytics/${websiteId}?range=7d`, {
      headers: {
        'X-API-Key': API_KEY,
      }
    })
  );
}

Debugging Techniques

Enable Debug Mode

// Log all API requests
const DEBUG = process.env.ZENOVAY_DEBUG === 'true';

async function apiRequest(endpoint) {
  if (DEBUG) {
    console.log(`[Zenovay] Request: ${endpoint}`);
  }

  const response = await fetch(`https://api.zenovay.com/api/external/v1${endpoint}`, {
    headers: { 'X-API-Key': API_KEY },
  });
  const result = await response.json();

  if (DEBUG) {
    console.log(`[Zenovay] Response: ${response.status} [${response.headers.get('x-request-id')}]`, JSON.stringify(result, null, 2));
  }

  return result;
}

Request ID Tracking

Log the x-request-id response header whenever a request fails — support can use it to look up your exact request:

function handleError(error) {
  console.error(`API Error [${error.requestId}]:`, error.message);

  // Include in error reports
  if (typeof Sentry !== 'undefined') {
    Sentry.captureException(error, {
      extra: {
        requestId: error.requestId,
        code: error.code,
        status: error.status
      }
    });
  }
}

Validate Before Sending

function validateQuery(params) {
  const errors = [];

  if (!params.site_id) {
    errors.push('site_id is required');
  }

  if (params.range && !['24h', '7d', '30d', '90d', '1y'].includes(params.range)) {
    errors.push("range must be one of 24h, 7d, 30d, 90d, 1y");
  }

  if (errors.length > 0) {
    throw new Error(`Validation failed: ${errors.join(', ')}`);
  }

  return true;
}

Language-Specific Examples

Python

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

class ZenovayClient:
    def __init__(self, api_key):
        self.api_key = api_key
        self.base_url = 'https://api.zenovay.com/api/external/v1'

        # Configure retry strategy
        self.session = requests.Session()
        retry = Retry(
            total=3,
            backoff_factor=1,
            status_forcelist=[429, 500, 502, 503, 504]
        )
        adapter = HTTPAdapter(max_retries=retry)
        self.session.mount('https://', adapter)

    def get_analytics(self, website_id, time_range='7d'):
        response = self.session.get(
            f'{self.base_url}/analytics/{website_id}',
            params={'range': time_range},
            headers={
                'X-API-Key': self.api_key,
            },
            timeout=10
        )

        body = response.json()
        if not response.ok or body.get('success') is False:
            error = body.get('error', {})
            raise ZenovayAPIError(
                code=error.get('code'),
                message=error.get('message'),
                status=response.status_code,
                request_id=response.headers.get('x-request-id')
            )
        return body['data']

class ZenovayAPIError(Exception):
    def __init__(self, code, message, status, request_id=None):
        self.code = code
        self.message = message
        self.status = status
        self.request_id = request_id
        super().__init__(f'[{code}] {message}')

Ruby

require 'net/http'
require 'json'

class ZenovayClient
  class APIError < StandardError
    attr_reader :code, :status, :request_id

    def initialize(code:, message:, status:, request_id: nil)
      @code = code
      @status = status
      @request_id = request_id
      super(message)
    end
  end

  def initialize(api_key)
    @api_key = api_key
    @base_url = 'https://api.zenovay.com/api/external/v1'
  end

  def get_analytics(website_id, time_range = '7d')
    request("/analytics/#{website_id}?range=#{time_range}")
  end

  private

  def request(endpoint, retries: 3)
    uri = URI("#{@base_url}#{endpoint}")
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true

    req = Net::HTTP::Get.new(uri)
    req['X-API-Key'] = @api_key

    response = http.request(req)
    result = JSON.parse(response.body)

    if response.is_a?(Net::HTTPSuccess) && result['success'] != false
      result['data']
    else
      error = result['error'] || {}
      raise APIError.new(
        code: error['code'],
        message: error['message'],
        status: response.code.to_i,
        request_id: response['x-request-id']
      )
    end
  rescue APIError
    raise
  rescue StandardError => e
    retries -= 1
    retry if retries > 0
    raise e
  end
end

Error Monitoring

Integrate with Error Tracking

// Sentry integration
import * as Sentry from '@sentry/node';

async function fetchAnalytics(websiteId) {
  try {
    return await apiRequest(`/analytics/${websiteId}`);
  } catch (error) {
    Sentry.captureException(error, {
      tags: {
        api_error_code: error.code,
        endpoint: '/analytics'
      },
      extra: {
        request_id: error.requestId,
        website_id: websiteId
      }
    });
    throw error;
  }
}

Alert on Critical Errors

function handleError(error) {
  // Alert on authentication / plan issues
  if (error.code === 'UNAUTHORIZED' || error.code === 'API_PAID_PLAN_REQUIRED') {
    sendAlert('API authentication failed', error);
  }

  // Alert on quota exhaustion
  if (error.code === 'MONTHLY_LIMIT_EXCEEDED') {
    sendAlert('Monthly API quota reached', error);
  }

  // Log all errors
  console.error(`[${error.code}] ${error.message}`, {
    requestId: error.requestId,
    status: error.status
  });
}

Troubleshooting Checklist

Request Fails

  1. Check your API key is valid and starts with zv_
  2. Confirm your plan includes API access (Pro or higher)
  3. Verify the endpoint URL and base path (/api/external/v1)
  4. Validate query parameters (e.g. site_id, range)
  5. Check for network connectivity

Rate Limited

  1. Honor the Retry-After header before retrying
  2. Pace requests using the X-RateLimit-* headers
  3. Implement exponential backoff for 5xx responses
  4. Consider upgrading your plan if you consistently hit the limit

Validation Errors

  1. Read the error.message — it names the offending parameter
  2. Check required parameters are present
  3. Use a supported range value (24h, 7d, 30d, 90d, 1y)
  4. Keep query strings within documented length limits

Intermittent Failures

  1. Implement retry logic with backoff
  2. Use the circuit breaker pattern
  3. Check for network issues
  4. Monitor the Zenovay status page

Next Steps

Was this article helpful?