Aprende a gestionar los errores de la API de Zenovay de forma elegante con los códigos de error adecuados, estrategias de reintento y técnicas de depuración.
La API REST de Zenovay está disponible en los planes Pro, Scale y Enterprise. Las solicitudes autenticadas con una clave de plan gratuito se rechazan con un código 403 y API_PAID_PLAN_REQUIRED.
Formato de respuesta de error
Cada respuesta de API se envuelve en una bandera success. Los errores devuelven success: false con un objeto error:
{
"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"
}
}
El código de estado HTTP está en la respuesta misma (por ejemplo 429), no en el cuerpo.
Campos de respuesta
| Campo | Descripción |
|---|---|
success | false en caso de error, true en caso de éxito |
error.code | Código de error legible por máquinas (mayúsculas, por ejemplo RATE_LIMIT_EXCEEDED) |
error.message | Descripción del error legible por humanos |
error.timestamp | Marca de tiempo ISO 8601 de cuando se generó el error |
Cada respuesta también contiene un encabezado x-request-id (una UUID). Inclúyelo cuando contactes con soporte – nos permite rastrear una única solicitud de extremo a extremo.
Códigos de estado HTTP
Códigos de éxito (2xx)
| Código | Descripción |
|---|---|
200 | Solicitud exitosa |
Las respuestas exitosas utilizan el wrapper success: true:
{
"success": true,
"data": { /* ... */ },
"timestamp": "2026-06-13T10:30:00.000Z"
}
Errores del cliente (4xx)
| Código | Código de error | Descripción |
|---|---|---|
400 | VALIDATION_ERROR / MISSING_SITE_ID / GENERIC_ERROR | Parámetro inválido o faltante |
401 | UNAUTHORIZED | Clave API inválida o faltante |
403 | FORBIDDEN | La clave API no tiene acceso al recurso solicitado |
403 | API_PAID_PLAN_REQUIRED | La API requiere un plan de pago (las claves gratuitas están bloqueadas) |
404 | NOT_FOUND | Recurso no encontrado |
409 | CONFLICT | Conflicto de recurso (duplicado) |
429 | RATE_LIMIT_EXCEEDED | Demasiadas solicitudes este minuto |
429 | MONTHLY_LIMIT_EXCEEDED | Cuota de solicitudes mensual alcanzada |
Errores del servidor (5xx)
| Código | Código de error | Descripción |
|---|---|---|
500 | INTERNAL_ERROR / GENERIC_ERROR | Error del servidor |
Códigos de error comunes
Errores de autenticación
Una clave faltante, una clave mal formada (las claves de Zenovay comienzan con zv_) o una clave revocada devuelven todas 401 con el código UNAUTHORIZED. El mensaje te dice cuál:
// Clave API faltante
{
"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"
}
}
// Clave inválida o revocada
{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key",
"timestamp": "2026-06-13T10:30:00.000Z"
}
}
Auténticate con cualquiera encabezado:
Authorization: Bearer zv_YOUR_API_KEY
X-API-Key: zv_YOUR_API_KEY
Errores de validación
Los parámetros faltantes o mal formados devuelven una 400 con un mensaje de texto plano que describe el problema:
// Parámetro de consulta requerido faltante
{
"success": false,
"error": {
"code": "MISSING_SITE_ID",
"message": "site_id parameter is required",
"timestamp": "2026-06-13T10:30:00.000Z"
}
}
// Entrada inválida
{
"success": false,
"error": {
"code": "GENERIC_ERROR",
"message": "Query must be 500 characters or less",
"timestamp": "2026-06-13T10:30:00.000Z"
}
}
Errores de límite de tasa
Cuando excedes el límite de tasa por minuto, obtienes una 429 con RATE_LIMIT_EXCEEDED. Lee el encabezado de respuesta Retry-After (segundos) para saber cuánto tiempo esperar:
{
"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"
}
}
La respuesta incluye estos encabezados:
| Encabezado | Descripción |
|---|---|
Retry-After | Segundos a esperar antes de reintentar |
X-RateLimit-Limit | Tu límite de solicitud por minuto |
X-RateLimit-Reset | Marca de tiempo ISO 8601 cuando se reinicia la ventana |
Agotar la cuota mensual devuelve 429 con MONTHLY_LIMIT_EXCEEDED y encabezados X-Usage-* (X-Usage-Monthly, X-Usage-Limit, X-Usage-Reset).
Errores de recurso
Solicitar un sitio web al que no tienes acceso o que no existe devuelve 404 NOT_FOUND o 403 FORBIDDEN:
// Sitio web no encontrado
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Website not found",
"timestamp": "2026-06-13T10:30:00.000Z"
}
}
// La clave no tiene acceso a este sitio web
{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "API key does not have access to this website",
"timestamp": "2026-06-13T10:30:00.000Z"
}
}
Errores de límite de plan
Algunos puntos de terminación requieren un plan superior al tuyo. Las claves gratuitas están completamente bloqueadas; las claves Pro están bloqueadas de los puntos de terminaison solo para Scale (por ejemplo, la API de consulta de lenguaje natural):
// Clave gratuita llamando a cualquier punto de terminaison de API
{
"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"
}
}
// Clave Pro llamando a un punto de terminaison solo para Scale
{
"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"
}
}
Estrategias de manejo de errores
Manejo básico de errores
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 {
// Error de red
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}`);
}
}
Reintento con retroceso exponencial
Reintenta respuestas 429 y 5xx. Para 429, honra el encabezado Retry-After en lugar de adivinar:
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;
}
// No reintentar errores del cliente no-reintentables
if (!retryableStatus.includes(response.status) || attempt === maxRetries) {
throw new ZenovayAPIError(body.error, response.status, response.headers.get('x-request-id'));
}
// Prefiere el Retry-After del servidor (segundos) en 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); // Máx 30s
}
console.log(`Retry attempt ${attempt + 1} after ${delay}ms`);
await sleep(delay);
} catch (error) {
if (error instanceof ZenovayAPIError) {
throw error;
}
// Error de red - reintentar
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;
}
}
Manejo de límite de tasa
Ajusta tus solicitudes usando los encabezados de límite de tasa para retroceder antes de un 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;
// Espera si está limitado por tasa
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,
}
});
// Comprobar encabezados de límite de tasa
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);
// Volver a encolar la solicitud
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());
}
}
}
}
Patrón Circuit Breaker
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;
}
}
}
// Uso
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,
}
})
);
}
Técnicas de depuración
Habilitar modo de depuración
// Registrar todas las solicitudes de la API
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;
}
Seguimiento de ID de solicitud
Siempre registra el encabezado de respuesta x-request-id cuando una solicitud falla – el soporte puede utilizarlo para buscar tu solicitud exacta:
function handleError(error) {
console.error(`API Error [${error.requestId}]:`, error.message);
// Incluir en informes de error
if (typeof Sentry !== 'undefined') {
Sentry.captureException(error, {
extra: {
requestId: error.requestId,
code: error.code,
status: error.status
}
});
}
}
Validar antes de enviar
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;
}
Ejemplos específicos del lenguaje
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'
# Configurar estrategia de reintento
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
Monitoreo de errores
Integración con rastreo de errores
// Integración de Sentry
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;
}
}
Alertas en errores críticos
function handleError(error) {
// Alerta sobre problemas de autenticación / plan
if (error.code === 'UNAUTHORIZED' || error.code === 'API_PAID_PLAN_REQUIRED') {
sendAlert('API authentication failed', error);
}
// Alerta sobre agotamiento de cuota
if (error.code === 'MONTHLY_LIMIT_EXCEEDED') {
sendAlert('Monthly API quota reached', error);
}
// Registrar todos los errores
console.error(`[${error.code}] ${error.message}`, {
requestId: error.requestId,
status: error.status
});
}
Lista de verificación de solución de problemas
La solicitud falla
- Verifica que tu clave API sea válida y comience con
zv_ - Confirma que tu plan incluye acceso a la API (Pro o superior)
- Verifica la URL del punto de terminaison y la ruta base (
/api/external/v1) - Valida los parámetros de consulta (por ejemplo
site_id,range) - Comprueba la conectividad de red
Limitado por tasa
- Honra el encabezado
Retry-Afterantes de reintentar - Ajusta las solicitudes usando los encabezados
X-RateLimit-* - Implementa retroceso exponencial para respuestas
5xx - Considera actualizar tu plan si constantemente excedes el límite
Errores de validación
- Lee la
error.message– indica el parámetro contravención - Verifica que los parámetros requeridos están presentes
- Usa un valor
rangesoportado (24h,7d,30d,90d,1y) - Mantén las cadenas de consulta dentro de los límites de longitud documentados
Fallos intermitentes
- Implementa lógica de reintento con retroceso
- Usa el patrón circuit breaker
- Comprueba problemas de red
- Monitorea la página de estado de Zenovay