Skip to content
ApeChain: Checking updates
ApeChain

Checking updates

Checking for new indexed heads.

Latest block
52,536,012
Updated

ApeChain API

Public JSON over HTTPS. No key needed.

Quick start

Base URL
https://explorer.apechain.com
Etherscan-compatible
https://explorer.apechain.com/api
Live blocks (WebSocket)
wss://explorer.apechain.com/ws
Rate limits
Per IP, per second; higher with an x-api-key header. Every response carries x-ratelimit-remaining.
OpenAPI
openapi.json · version 0.1.0

Build with AI

Connect Claude, Cursor or another MCP client to read ApeScan data in plain language. No account or API key is needed.

{
  "mcpServers": {
    "apescan": {
      "url": "https://explorer.apechain.com/mcp"
    }
  }
}

Read-only tools: blocks, transactions, addresses, tokens, NFTs, search, logs, verified contracts and API documentation.

Endpoints

First request

One NFT: metadata, owner and traits.

Native JSON · /v1
GETItem request
curl -sS 'https://explorer.apechain.com/v1/nfts/0xbebaa24108d6a03c7331464270b95278bbbe6ff7/2202'
Public · ApeChain 33139Open GET ↗
Example response—

Example unavailable. Run the request ↗

Loading example…

Pagination and errors

  • Cursors: pass next back as cursor with the same filters.
  • Coverage: check coverage.complete for full history.
  • 400 bad_request: fix the query; do not retry unchanged.
  • 410 cursor_expired (/v1/nfts/collections and /v1/search/tokens only): restart from the first page.
  • 429: rate limited; wait for retry-after.
  • 500 internal: server error; retry later.
  • 503 lookup_unavailable: the node is unreachable; retry after retry-after.
Full client
// Runtime: Browser or Node 18+ JavaScript. Copy this complete artifact into your app.
const DEFAULT_RETRY_AFTER_MS = 2000;
const MAX_RETRY_AFTER_MS = 30000;
function retryAfterMs(response) {
  const raw = response.headers.get("retry-after")?.trim() ?? "";
  if (!/^\d+(?:\.\d+)?$/.test(raw)) return DEFAULT_RETRY_AFTER_MS;
  return Math.min(MAX_RETRY_AFTER_MS, Math.ceil(Number(raw) * 1000));
}
function wait(ms, signal) {
  return new Promise((resolve, reject) => {
    if (signal?.aborted) return reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
    const timer = setTimeout(done, ms);
    function done() { signal?.removeEventListener("abort", aborted); resolve(); }
    function aborted() { clearTimeout(timer); signal?.removeEventListener("abort", aborted); reject(signal?.reason ?? new DOMException("Aborted", "AbortError")); }
    signal?.addEventListener("abort", aborted, { once: true });
  });
}
class PaginationError extends Error {
  constructor(status, state) { super(`ApeScan ${status}; resume at ${state.url}`); this.status = status; this.state = state; }
}
class PaginationTransportError extends Error {
  constructor(state, cause) { super(`ApeScan transport failed; resume at ${state.url}`); this.state = state; this.cause = cause; }
}
class PaginationConsumerError extends Error {
  constructor(request, nextUrl, cause) { super("Page consumer failed; committed progress is unknown"); this.request = request; this.nextUrl = nextUrl; this.committed = "unknown"; this.cause = cause; }
}
class PaginationProgressError extends Error {
  constructor(reason, request, nextUrl, processedPages) {
    super(`Pagination incomplete: ${reason}`); this.reason = reason; this.request = request; this.nextUrl = nextUrl;
    this.processedPages = processedPages; this.complete = false; this.committed = reason === "page_limit" ? "processed" : "not_consumed";
    this.resumeUrl = reason === "page_limit" ? nextUrl : request.url;
  }
}
function validateBounds(maxPages, maxRetries) {
  if (!Number.isSafeInteger(maxPages) || maxPages < 1 || maxPages > 1000 || !Number.isSafeInteger(maxRetries)
    || maxRetries < 0 || !Number.isSafeInteger(maxPages * (maxRetries + 1))) throw new RangeError("Invalid pagination work bounds");
}
function validPage(page) {
  if (!page || typeof page !== "object" || Array.isArray(page)) return false;
  const d = Object.getOwnPropertyDescriptors(page), items = d.items, next = d.next;
  return !!items && Object.hasOwn(items, "value") && Array.isArray(items.value) && !!next && Object.hasOwn(next, "value")
    && (next.value === null || typeof next.value === "string" && next.value.length > 0);
}
function stateFor(url) {
  const current = new URL(url);
  return { url, cursor: current.searchParams.get("cursor") };
}
function continuationFor(url, cursor) {
  if (cursor === null) return null;
  const current = new URL(url), query = current.search ? current.search.slice(1).split("&") : [];
  const replacement = new URLSearchParams({ cursor }).toString();
  const at = query.findIndex((part) => new URLSearchParams(part).has("cursor"));
  if (at >= 0) query[at] = replacement; else query.push(replacement);
  current.search = query.join("&");
  return current.toString();
}
// Default 100 pages, ceiling 1000; safe total attempts <= maxPages*(maxRetries+1).
// This is a local engineering work bound, not a deadline or backend SLA.
async function runPagination({ url, fetcher = fetch, sleep = wait, signal, maxRetries = 3, maxPages = 100, onPage = async () => {} }) {
  validateBounds(maxPages, maxRetries);
  if (new URL(url).searchParams.getAll("cursor").length > 1) throw new TypeError("Ambiguous initial cursor");
  let nextUrl = url;
  const requested = new Set();
  let processedPages = 0;
  while (nextUrl) {
    requested.add(stateFor(nextUrl).cursor);
    let retries = 0;
    for (;;) {
      if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
      const state = stateFor(nextUrl);
      let response;
      try {
        response = await fetcher(nextUrl, { signal });
      } catch (cause) {
        if (signal?.aborted) throw signal.reason ?? cause;
        throw new PaginationTransportError(state, cause);
      }
      if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
      if ((response.status === 429 || response.status === 503) && retries++ < maxRetries) {
        try { await sleep(retryAfterMs(response), signal); } catch (cause) {
          if (signal?.aborted) throw signal.reason ?? cause;
          throw cause;
        }
        continue;
      }
      if (!response.ok) throw new PaginationError(response.status, state);
      if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
      let page;
      try { page = await response.json(); } catch (cause) {
        if (signal?.aborted) throw signal.reason ?? cause;
        throw new PaginationProgressError("malformed_page", state, null, processedPages);
      }
      if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
      if (!validPage(page)) throw new PaginationProgressError("malformed_page", state, null, processedPages);
      const continuation = continuationFor(nextUrl, page.next);
      if (page.next !== null && requested.has(page.next)) throw new PaginationProgressError("cursor_cycle", state, continuation, processedPages);
      try {
        await onPage(page, state);
      } catch (cause) {
        if (signal?.aborted) throw signal.reason ?? cause;
        throw new PaginationConsumerError(state, continuation, cause);
      }
      if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
      processedPages++;
      if (!continuation) return { complete: true, processedPages, resumeUrl: null };
      if (processedPages >= maxPages) throw new PaginationProgressError("page_limit", state, continuation, processedPages);
      nextUrl = continuation;
      break;
    }
  }
  return { complete: true, processedPages, resumeUrl: null };
}
const controller = new AbortController();
const pagination = {
  controller,
  run: (options = {}) => runPagination({
    url: "https://explorer.apechain.com/v1/nfts/collections?limit=100",
    signal: controller.signal,
    ...options,
  }),
};
// Example: await pagination.run({ onPage(page, state) { for (const item of page.items) console.log(item); console.info("resume URL:", state.url); } });
// Progress errors include request, nextUrl, processedPages, committed and an explicit resumeUrl.
// A cycle or malformed page is not consumed. Its resumeUrl names the unconsumed request, not a safe automatic retry.
// A page-limit error follows successful consumption and resumes at nextUrl. Never automatically refetch committed pages.
// Call pagination.controller.abort() to cancel fetches and retry waits. Cancellation keeps its original AbortError and has no resume state.