適切なエラーコード、リトライ戦略、デバッグテクニックを使って、Zenovay APIのエラーを適切に処理する方法をご説明します。
Zenovay REST APIはPro、Scale、Enterpriseプランで利用可能です。フリープランのキーで認証された要求は、403とAPI_PAID_PLAN_REQUIREDコードで拒否されます。
エラーレスポンスの形式
すべてのAPIレスポンスはsuccessフラグでラップされています。エラーはsuccess: falseと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"
}
}
HTTPステータスコードはレスポンス自体(例:429)にあり、ボディ内にはありません。
レスポンスフィールド
| フィールド | 説明 |
|---|---|
success | エラー時はfalse、成功時はtrue |
error.code | 機械可読なエラーコード(大文字、例:RATE_LIMIT_EXCEEDED) |
error.message | 人間が読めるエラーの説明 |
error.timestamp | エラーが生成された時刻のISO 8601タイムスタンプ |
また、すべてのレスポンスにはx-request-idヘッダ(UUID)が含まれます。サポートに問い合わせる際はこれを含めてください — 単一のリクエストを追跡できます。
HTTPステータスコード
成功コード(2xx)
| コード | 説明 |
|---|---|
200 | リクエスト成功 |
成功レスポンスはsuccess: trueラッパーを使用します:
{
"success": true,
"data": { /* ... */ },
"timestamp": "2026-06-13T10:30:00.000Z"
}
クライアントエラー(4xx)
| コード | エラーコード | 説明 |
|---|---|---|
400 | VALIDATION_ERROR / MISSING_SITE_ID / GENERIC_ERROR | 無効または不足しているパラメータ |
401 | UNAUTHORIZED | 無効または不足しているAPIキー |
403 | FORBIDDEN | APIキーが要求されたリソースにアクセスできない |
403 | API_PAID_PLAN_REQUIRED | APIは有料プランが必要(フリーキーはブロック) |
404 | NOT_FOUND | リソースが見つからない |
409 | CONFLICT | リソースの競合(重複) |
429 | RATE_LIMIT_EXCEEDED | この分間のリクエストが多すぎる |
429 | MONTHLY_LIMIT_EXCEEDED | 月次リクエストクォータに達した |
サーバーエラー(5xx)
| コード | エラーコード | 説明 |
|---|---|---|
500 | INTERNAL_ERROR / GENERIC_ERROR | サーバーエラー |
よくあるエラーコード
認証エラー
不足しているキー、形式が悪いキー(Zenovayキーはzv_で始まります)、または取り消されたキーはすべて401とUNAUTHORIZEDコードを返します。メッセージがどれかを示します:
// APIキーが不足している
{
"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"
}
}
// 無効または取り消されたキー
{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid API key",
"timestamp": "2026-06-13T10:30:00.000Z"
}
}
どちらかのヘッダで認証します:
Authorization: Bearer zv_YOUR_API_KEY
X-API-Key: zv_YOUR_API_KEY
バリデーションエラー
不足しているまたは形式が悪いパラメータは、問題を説明するプレーンテキストメッセージ付きで400を返します:
// 必須クエリパラメータが不足している
{
"success": false,
"error": {
"code": "MISSING_SITE_ID",
"message": "site_id parameter is required",
"timestamp": "2026-06-13T10:30:00.000Z"
}
}
// 無効な入力
{
"success": false,
"error": {
"code": "GENERIC_ERROR",
"message": "Query must be 500 characters or less",
"timestamp": "2026-06-13T10:30:00.000Z"
}
}
レート制限エラー
1分あたりのレート制限を超えると、RATE_LIMIT_EXCEEDEDで429を取得します。Retry-Afterレスポンスヘッダ(秒数)を読んで、どのくらい待つ必要があるかを確認します:
{
"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"
}
}
レスポンスには次のヘッダが含まれます:
| ヘッダ | 説明 |
|---|---|
Retry-After | リトライ前に待機する秒数 |
X-RateLimit-Limit | 1分あたりのリクエスト制限 |
X-RateLimit-Reset | ウィンドウがリセットされるISO 8601タイムスタンプ |
月次クォータを使い切ると、MONTHLY_LIMIT_EXCEEDEDとX-Usage-*ヘッダ(X-Usage-Monthly、X-Usage-Limit、X-Usage-Reset)で429が返されます。
リソースエラー
アクセス権のないウェブサイト、または存在しないウェブサイトをリクエストすると、404 NOT_FOUNDまたは403 FORBIDDENが返されます:
// ウェブサイトが見つからない
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Website not found",
"timestamp": "2026-06-13T10:30:00.000Z"
}
}
// キーはこのウェブサイトにアクセスできない
{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "API key does not have access to this website",
"timestamp": "2026-06-13T10:30:00.000Z"
}
}
プラン制限エラー
一部のエンドポイントは現在のプランより高いプランが必要です。フリーキーは完全にブロックされており、ProキーはScale専用エンドポイントからブロックされています(例:自然言語クエリAPI):
// 任意の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"
}
}
// Scale専用エンドポイントを呼び出すProキー
{
"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"
}
}
エラー処理戦略
基本的なエラー処理
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 {
// ネットワークエラー
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}`);
}
}
指数バックオフによるリトライ
429と5xxレスポンスをリトライします。429の場合、推測する代わりにRetry-Afterヘッダを尊重します:
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;
}
// リトライ不可能なクライアントエラーはリトライしない
if (!retryableStatus.includes(response.status) || attempt === maxRetries) {
throw new ZenovayAPIError(body.error, response.status, response.headers.get('x-request-id'));
}
// 429の場合はサーバーのRetry-After(秒)を優先
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); // 最大30秒
}
console.log(`Retry attempt ${attempt + 1} after ${delay}ms`);
await sleep(delay);
} catch (error) {
if (error instanceof ZenovayAPIError) {
throw error;
}
// ネットワークエラー - リトライ
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;
}
}
レート制限処理
レート制限ヘッダを使用してリクエストをペースし、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;
// レート制限中は待機
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,
}
});
// レート制限ヘッダを確認
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);
// リクエストを再度キューに入れ
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());
}
}
}
}
サーキットブレーカーパターン
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;
}
}
}
// 使用例
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,
}
})
);
}
デバッグテクニック
デバッグモードの有効化
// すべての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;
}
リクエストIDのトラッキング
リクエストが失敗したときは必ずx-request-idレスポンスヘッダをログに記録してください — サポートはそれを使用して正確なリクエストを検索できます:
function handleError(error) {
console.error(`API Error [${error.requestId}]:`, error.message);
// エラーレポートに含める
if (typeof Sentry !== 'undefined') {
Sentry.captureException(error, {
extra: {
requestId: error.requestId,
code: error.code,
status: error.status
}
});
}
}
送信前に検証
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;
}
言語別の例
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'
# リトライ戦略を構成
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
エラー監視
エラー追跡との統合
// 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;
}
}
重大エラーのアラート
function handleError(error) {
// 認証/プランの問題についてアラート
if (error.code === 'UNAUTHORIZED' || error.code === 'API_PAID_PLAN_REQUIRED') {
sendAlert('API authentication failed', error);
}
// クォータ枯渇についてアラート
if (error.code === 'MONTHLY_LIMIT_EXCEEDED') {
sendAlert('Monthly API quota reached', error);
}
// すべてのエラーをログ記録
console.error(`[${error.code}] ${error.message}`, {
requestId: error.requestId,
status: error.status
});
}
トラブルシューティングチェックリスト
リクエストが失敗する
- APIキーが有効で
zv_で始まることを確認する - プランにAPI アクセスが含まれていることを確認する(Pro以上)
- エンドポイントURLとベースパス(
/api/external/v1)を確認する - クエリパラメータを検証する(例:
site_id、range) - ネットワーク接続を確認する
レート制限に達した
- リトライ前に
Retry-Afterヘッダを尊重する X-RateLimit-*ヘッダを使用してリクエストをペースする5xxレスポンスに対して指数バックオフを実装する- 継続的に制限に達する場合はプランアップグレードを検討する
バリデーションエラーが発生する
error.messageを読む — 問題のパラメータが示されている- 必須パラメータが存在することを確認する
- サポートされている
range値を使用する(24h、7d、30d、90d、1y) - クエリ文字列を文書化された長さ制限内に保つ
断続的な障害
- バックオフ付きのリトライロジックを実装する
- サーキットブレーカーパターンを使用する
- ネットワークの問題を確認する
- Zenovayステータスページを監視する