Errors

The envelope

Every non-2xx response carries the same JSON envelope, regardless of which endpoint you hit:

json
{
"error": {
"code": "quota_exceeded",
"message": "Monthly request quota exceeded.",
"request_id": "req_01HT...",
"details": { "resets_at": "2026-05-01" }
}
}
  • error.code is a stable, machine-readable string. Switch on this, not on error.message.
  • error.message is a human-readable summary, subject to wording changes without notice.
  • error.request_id matches the X-Request-Id response header — include it when you file a support ticket.
  • error.details is endpoint- and code-specific; see each row below.

The full code list

HTTPerror.codeMeaning
400invalid_parameterMissing or malformed query parameter.
401invalid_api_keyKey missing, malformed, or revoked.
403plan_forbiddenValid key, but your plan doesn't include this endpoint or feature (e.g. hourly-data).
404not_foundNo such route. Unknown exchanges, symbols, or tokens return an empty data: [] instead.
409key_limit_reachedTried to mint a third active key.
415unsupported_media_typeAsked for a response format we don't produce.
422unprocessableValid shape but semantically invalid (e.g. start > end).
429rate_limitedPer-second burst limit. Honour Retry-After and try again.
429quota_exceededMonthly hard cap. Wait until details.resets_at.
500internal_errorServer bug. File a ticket with request_id.
503temporarily_unavailableUpstream DB degraded. Retry with backoff.

How to retry

  • Retry (with exponential backoff and jitter): 429 rate_limited, 503 temporarily_unavailable, transport-level failures.
  • Don't retry: any other 4xx code. Fix the request.
  • Stop until reset: 429 quota_exceeded. Read details.resets_at.

Example: idiomatic TypeScript handler

typescript
async function callZm(path: string, body: Record<string, unknown>) {
const url = `https://zealous.markets/api${path}`;
for (let attempt = 0; attempt < 4; attempt++) {
const res = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ZM_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (res.ok) return res.json();
const body = await res.json().catch(() => ({}));
const code = body?.error?.code;
if (code === 'rate_limited' || res.status === 503) {
const retryAfter = Number(res.headers.get('retry-after') ?? 1);
await new Promise((r) => setTimeout(r, retryAfter * 1000 * 2 ** attempt));
continue;
}
throw new Error(
`${res.status} ${code}: ${body?.error?.message} (req=${body?.error?.request_id})`,
);
}
throw new Error('too many retries');
}