Aprenda a tratar erros da API do Zenovay de forma elegante com códigos de erro adequados, estratégias de retry e técnicas de depuração.
A API REST do Zenovay está disponível nos planos Pro, Scale e Enterprise. Requisições autenticadas com uma chave de plano gratuito são rejeitadas com um código 403 e API_PAID_PLAN_REQUIRED.
Formato de Resposta de Erro
Cada resposta de API é envolvida em uma flag success. Erros retornam success: false com um 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"
}
}
O código de status HTTP está na resposta em si (por exemplo 429), não no corpo.
Campos da Resposta
| Campo | Descrição |
|---|---|
success | false em caso de erro, true em caso de sucesso |
error.code | Código de erro legível por máquina (maiúsculas, por exemplo RATE_LIMIT_EXCEEDED) |
error.message | Descrição do erro legível por humanos |
error.timestamp | Marca de tempo ISO 8601 de quando o erro foi gerado |
Cada resposta também inclui um cabeçalho x-request-id (um UUID). Inclua-o ao contactar o suporte – isso nos permite rastrear uma única requisição de ponta a ponta.
Códigos de Status HTTP
Códigos de Sucesso (2xx)
| Código | Descrição |
|---|---|
200 | Requisição bem-sucedida |
Respostas bem-sucedidas usam o wrapper success: true:
{
"success": true,
"data": { /* ... */ },
"timestamp": "2026-06-13T10:30:00.000Z"
}
Erros do Cliente (4xx)
| Código | Código de Erro | Descrição |
|---|---|---|
400 | VALIDATION_ERROR / MISSING_SITE_ID / GENERIC_ERROR | Parâmetro inválido ou ausente |
401 | UNAUTHORIZED | Chave de API inválida ou ausente |
403 | FORBIDDEN | Chave de API não tem acesso ao recurso solicitado |
403 | API_PAID_PLAN_REQUIRED | A API requer um plano pago (chaves gratuitas são bloqueadas) |
404 | NOT_FOUND | Recurso não encontrado |
409 | CONFLICT | Conflito de recurso (duplicado) |
429 | RATE_LIMIT_EXCEEDED | Muitas requisições este minuto |
429 | MONTHLY_LIMIT_EXCEEDED | Cota mensal de requisições alcançada |
Erros do Servidor (5xx)
| Código | Código de Erro | Descrição |
|---|---|---|
500 | INTERNAL_ERROR / GENERIC_ERROR | Erro do servidor |
Códigos de Erro Comuns
Erros de Autenticação
Uma chave ausente, uma chave mal formada (chaves do Zenovay começam com zv_) ou uma chave revogada retornam todas 401 com o código UNAUTHORIZED. A mensagem indica qual:
// Chave de API ausente
{
"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"
}
}
// Chave inválida ou revogada
{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key",
"timestamp": "2026-06-13T10:30:00.000Z"
}
}
Autentique-se com qualquer um cabeçalho:
Authorization: Bearer zv_YOUR_API_KEY
X-API-Key: zv_YOUR_API_KEY
Erros de Validação
Parâmetros ausentes ou mal formados retornam uma 400 com uma mensagem de texto simples descrevendo o problema:
// Parâmetro de consulta obrigatório ausente
{
"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"
}
}
Erros de Limite de Taxa
Quando você excede o limite de taxa por minuto, obtém uma 429 com RATE_LIMIT_EXCEEDED. Leia o cabeçalho de resposta Retry-After (segundos) para saber quanto tempo 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"
}
}
A resposta inclui estes cabeçalhos:
| Cabeçalho | Descrição |
|---|---|
Retry-After | Segundos a esperar antes de tentar novamente |
X-RateLimit-Limit | Seu limite de requisição por minuto |
X-RateLimit-Reset | Marca de tempo ISO 8601 quando a janela se reinicia |
Exaurir a cota mensal retorna 429 com MONTHLY_LIMIT_EXCEEDED e cabeçalhos X-Usage-* (X-Usage-Monthly, X-Usage-Limit, X-Usage-Reset).
Erros de Recurso
Solicitar um site que você não tem acesso ou que não existe retorna 404 NOT_FOUND ou 403 FORBIDDEN:
// Site não encontrado
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Website not found",
"timestamp": "2026-06-13T10:30:00.000Z"
}
}
// A chave não tem acesso a este site
{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "API key does not have access to this website",
"timestamp": "2026-06-13T10:30:00.000Z"
}
}
Erros de Limite de Plano
Alguns endpoints requerem um plano superior ao seu. Chaves gratuitas são completamente bloqueadas; chaves Pro são bloqueadas de endpoints apenas para Scale (por exemplo, a API de consulta em linguagem natural):
// Chave gratuita chamando qualquer endpoint 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"
}
}
// Chave Pro chamando um endpoint apenas 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"
}
}
Estratégias de Tratamento de Erros
Tratamento Básico de Erros
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 {
// Erro de rede
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 com Backoff Exponencial
Tente novamente respostas 429 e 5xx. Para 429, honre o cabeçalho Retry-After em vez de adivinhar:
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;
}
// Não tente novamente erros de cliente não-retriáveis
if (!retryableStatus.includes(response.status) || attempt === maxRetries) {
throw new ZenovayAPIError(body.error, response.status, response.headers.get('x-request-id'));
}
// Prefira o Retry-After do servidor (segundos) em 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;
}
// Erro de rede - tente novamente
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;
}
}
Tratamento de Limite de Taxa
Ajuste suas requisições usando os cabeçalhos de limite de taxa para se afastar antes de um 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;
// Aguarde se estiver com limite de taxa
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,
}
});
// Verificar cabeçalhos de limite de taxa
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);
// Recolocar a requisição na fila
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());
}
}
}
}
Padrão 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 Depuração
Habilitar Modo Debug
// Registre todas as requisições da 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;
}
Rastreamento de ID de Requisição
Sempre registre o cabeçalho de resposta x-request-id quando uma requisição falhar – o suporte pode usá-lo para procurar sua requisição exata:
function handleError(error) {
console.error(`API Error [${error.requestId}]:`, error.message);
// Incluir em relatórios de erro
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;
}
Exemplos por Linguagem
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 estratégia de retry
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
Monitoramento de Erros
Integrar com Rastreamento de Erros
// Integração 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 para Erros Críticos
function handleError(error) {
// Alerte sobre problemas de autenticação / plano
if (error.code === 'UNAUTHORIZED' || error.code === 'API_PAID_PLAN_REQUIRED') {
sendAlert('API authentication failed', error);
}
// Alerte sobre esgotamento de cota
if (error.code === 'MONTHLY_LIMIT_EXCEEDED') {
sendAlert('Monthly API quota reached', error);
}
// Registre todos os erros
console.error(`[${error.code}] ${error.message}`, {
requestId: error.requestId,
status: error.status
});
}
Checklist de Solução de Problemas
Requisição Falha
- Verifique se sua chave de API é válida e começa com
zv_ - Confirme que seu plano inclui acesso à API (Pro ou superior)
- Verifique a URL do endpoint e o caminho base (
/api/external/v1) - Valide os parâmetros de consulta (por exemplo
site_id,range) - Verifique a conectividade de rede
Limitado por Taxa
- Honre o cabeçalho
Retry-Afterantes de tentar novamente - Ajuste as requisições usando os cabeçalhos
X-RateLimit-* - Implemente backoff exponencial para respostas
5xx - Considere fazer upgrade do seu plano se consistentemente exceder o limite
Erros de Validação
- Leia a
error.message– ela nomeia o parâmetro ofensivo - Verifique se os parâmetros obrigatórios estão presentes
- Use um valor
rangesuportado (24h,7d,30d,90d,1y) - Mantenha as strings de consulta dentro dos limites de comprimento documentados
Falhas Intermitentes
- Implemente lógica de retry com backoff
- Use o padrão circuit breaker
- Verifique problemas de rede
- Monitore a página de status do Zenovay