Skip to main content
Query Immutable Chain data—NFTs, tokens, activities, and orders—via APIs and webhooks, without running your own infrastructure.

Why Use Indexer?

Query blockchain data via simple API calls. No need to run nodes, databases, or indexing pipelines.
Data indexed within seconds of on-chain confirmation. Webhooks push updates instantly.
Filter by owner, collection, attributes, activity type, and more. Get exactly the data you need.
Handles millions of queries daily across the Immutable ecosystem.

Data Available

CategoryExamples
NFTsOwnership, metadata, attributes, images
TokensERC-20 balances, transfers
ActivitiesMints, transfers, burns, sales
CollectionsStats, metadata, floor prices
OrdersActive listings and bids

Integration Patterns

Choose the right pattern for your use case:

Polling vs Webhooks

PatternBest ForTrade-offs
PollingPlayer-driven requests, low volumeSimple, but may hit rate limits
WebhooksServer sync, real-time updatesEfficient, requires server infrastructure
Use polling when:
  • Player opens inventory (fetch their NFTs)
  • Player views an item (fetch metadata)
  • Player searches marketplace (query listings)
import { BlockchainData } from '@imtbl/blockchain-data';
import { Environment } from '@imtbl/config';

const indexer = new BlockchainData({
  baseConfig: { environment: Environment.SANDBOX },
});

// Player opens inventory → fetch their NFTs
async function loadInventory(address: string) {
  const { result } = await indexer.listNFTsByAccountAddress({
    chainName: 'imtbl-zkevm-testnet',
    accountAddress: address,
  });
  return result;
}

Caching Strategy

Reduce API calls with smart caching:
const CACHE_TTL = {
  nftMetadata: 24 * 60 * 60 * 1000, // 24 hours - rarely changes
  nftOwnership: 30 * 1000,          // 30 seconds - changes on transfers
  collectionStats: 5 * 60 * 1000,   // 5 minutes - aggregated data
  activities: 0,                     // No cache - always fresh
};

class IndexerCache {
  private cache = new Map<string, { data: any; expires: number }>();

  async get<T>(key: string, ttl: number, fetcher: () => Promise<T>): Promise<T> {
    const cached = this.cache.get(key);
    if (cached && cached.expires > Date.now()) {
      return cached.data as T;
    }
    
    const data = await fetcher();
    this.cache.set(key, { data, expires: Date.now() + ttl });
    return data;
  }
}

// Usage
const cache = new IndexerCache();

async function getNFTMetadata(contractAddress: string, tokenId: string) {
  return cache.get(
    `nft:${contractAddress}:${tokenId}`,
    CACHE_TTL.nftMetadata,
    () => indexer.getNFT({ chainName: 'imtbl-zkevm-testnet', contractAddress, tokenId })
  );
}

Pagination

For large result sets, paginate efficiently:
async function getAllNFTs(contractAddress: string): Promise<NFT[]> {
  const allNFTs: NFT[] = [];
  let cursor: string | undefined;
  
  do {
    const { result, page } = await indexer.listNFTs({
      chainName: 'imtbl-zkevm-testnet',
      contractAddress,
      pageSize: 200, // Max page size
      pageCursor: cursor,
    });
    
    allNFTs.push(...result);
    cursor = page.nextCursor;
  } while (cursor);
  
  return allNFTs;
}
Full collection syncs can be slow. Use webhooks for keeping a database in sync rather than periodic full fetches.

Rate Limit Handling

async function fetchWithRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3
): Promise<T> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error: any) {
      if (error.status === 429 && i < maxRetries - 1) {
        // Rate limited - exponential backoff
        await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
        continue;
      }
      throw error;
    }
  }
  throw new Error('Max retries exceeded');
}
TierRate Limit
Standard50 req/sec
EnterpriseCustom

Common Patterns

Player Inventory

async function getPlayerInventory(address: string, contractAddress?: string) {
  const { result } = await indexer.listNFTsByAccountAddress({
    chainName: 'imtbl-zkevm-testnet',
    accountAddress: address,
    contractAddress, // Optional: filter to specific collection
  });
  
  return result.map(nft => ({
    id: nft.token_id,
    name: nft.name,
    image: nft.image,
    attributes: nft.attributes,
  }));
}

Activity Feed

async function getRecentActivity(contractAddress: string) {
  const { result } = await indexer.listActivities({
    chainName: 'imtbl-zkevm-testnet',
    contractAddress,
    activityType: 'sale', // or 'mint', 'transfer', 'burn'
    pageSize: 20,
  });
  
  return result.map(activity => ({
    type: activity.activity_type,
    from: activity.from,
    to: activity.to,
    tokenId: activity.token_id,
    timestamp: activity.indexed_at,
    price: activity.details?.amount,
  }));
}

Collection Stats

async function getCollectionOverview(contractAddress: string) {
  const collection = await indexer.getCollection({
    chainName: 'imtbl-zkevm-testnet',
    contractAddress,
  });
  
  return {
    name: collection.name,
    totalSupply: collection.total_supply,
    holders: collection.distinct_owners,
    // Note: floor price comes from Orderbook, not Indexer
  };
}

Webhook Integration

Verifying Webhooks

Always verify webhook signatures:
import { createHmac } from 'crypto';

function verifyWebhook(payload: string, signature: string, secret: string): boolean {
  const expected = createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return signature === `sha256=${expected}`;
}

// Express middleware
app.post('/webhooks/immutable', (req, res) => {
  const signature = req.headers['x-immutable-signature'] as string;
  
  if (!verifyWebhook(JSON.stringify(req.body), signature, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  
  // Process the event
  const { event_name, data } = req.body;
  
  switch (event_name) {
    case 'imtbl_zkevm_activity_sale':
      handleSale(data);
      break;
    case 'imtbl_zkevm_activity_transfer':
      handleTransfer(data);
      break;
  }
  
  res.status(200).send('OK');
});

Database Sync Pattern

// When receiving a transfer webhook
async function handleTransfer(data: TransferEvent) {
  const { contract_address, token_id, from, to } = data;
  
  await db.nft.update({
    where: { contractAddress: contract_address, tokenId: token_id },
    data: { owner: to },
  });
  
  // Optionally fetch fresh data to ensure consistency
  const nft = await indexer.getNFT({
    chainName: 'imtbl-zkevm-testnet',
    contractAddress: contract_address,
    tokenId: token_id,
  });
  
  await db.nft.update({
    where: { contractAddress: contract_address, tokenId: token_id },
    data: { 
      owner: nft.owner,
      metadata: nft.metadata,
    },
  });
}
Webhooks are available to partners with a managed relationship. Contact us to enable webhooks.

Base URLs

EnvironmentURL
Testnethttps://api.sandbox.immutable.com
Mainnethttps://api.immutable.com

Next Steps