> ## Documentation Index
> Fetch the complete documentation index at: https://docs.immutable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Indexer

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

## Why Use Indexer?

<AccordionGroup>
  <Accordion title="Zero Infrastructure" icon="server">
    Query blockchain data via simple API calls. No need to run nodes, databases, or indexing pipelines.
  </Accordion>

  <Accordion title="Real-Time Data" icon="bolt">
    Data indexed within seconds of on-chain confirmation. Webhooks push updates instantly.
  </Accordion>

  <Accordion title="Rich Queries" icon="filter">
    Filter by owner, collection, attributes, activity type, and more. Get exactly the data you need.
  </Accordion>

  <Accordion title="Battle-Tested Scale" icon="chart-line">
    Handles millions of queries daily across the Immutable ecosystem.
  </Accordion>
</AccordionGroup>

## Data Available

| Category        | Examples                                |
| --------------- | --------------------------------------- |
| **NFTs**        | Ownership, metadata, attributes, images |
| **Tokens**      | ERC-20 balances, transfers              |
| **Activities**  | Mints, transfers, burns, sales          |
| **Collections** | Stats, metadata, floor prices           |
| **Orders**      | Active listings and bids                |

## Integration Patterns

Choose the right pattern for your use case:

### Polling vs Webhooks

| Pattern      | Best For                           | Trade-offs                                |
| ------------ | ---------------------------------- | ----------------------------------------- |
| **Polling**  | Player-driven requests, low volume | Simple, but may hit rate limits           |
| **Webhooks** | Server sync, real-time updates     | Efficient, requires server infrastructure |

<Tabs>
  <Tab title="Polling">
    Use polling when:

    * Player opens inventory (fetch their NFTs)
    * Player views an item (fetch metadata)
    * Player searches marketplace (query listings)

    ```typescript theme={null}
    import { BlockchainData } from '@imtbl/blockchain-data';
    import { Environment } from '@imtbl/config';

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

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

  <Tab title="Webhooks">
    Use webhooks when:

    * Syncing to your database
    * Triggering game events on sales/transfers
    * Real-time leaderboards or activity feeds

    Configure webhooks in [Hub](https://hub.immutable.com).

    | Event                           | Trigger              |
    | ------------------------------- | -------------------- |
    | `imtbl_zkevm_activity_mint`     | NFT minted           |
    | `imtbl_zkevm_activity_transfer` | NFT transferred      |
    | `imtbl_zkevm_activity_burn`     | NFT burned           |
    | `imtbl_zkevm_activity_sale`     | NFT sold             |
    | `imtbl_zkevm_order_updated`     | Order status changed |
  </Tab>
</Tabs>

### Caching Strategy

Reduce API calls with smart caching:

```typescript theme={null}
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:

```typescript theme={null}
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;
}
```

<Warning>
  Full collection syncs can be slow. Use webhooks for keeping a database in sync rather than periodic full fetches.
</Warning>

### Rate Limit Handling

```typescript theme={null}
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');
}
```

| Tier       | Rate Limit |
| ---------- | ---------- |
| Standard   | 50 req/sec |
| Enterprise | Custom     |

## Common Patterns

### Player Inventory

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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:

```typescript theme={null}
import crypto from 'crypto';

function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// In your webhook handler
app.post('/webhooks/immutable', (req, res) => {
  const signature = req.headers['x-immutable-signature'];
  const isValid = verifyWebhookSignature(
    JSON.stringify(req.body),
    signature,
    process.env.WEBHOOK_SECRET
  );
  
  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }
  
  // Process the webhook
  handleWebhookEvent(req.body);
  res.status(200).send('OK');
});
```

### Database Sync Pattern

```typescript theme={null}
// 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,
    },
  });
}
```

<Info>
  Webhooks are available to partners with a managed relationship. [Contact us](https://www.immutable.com/contact) to enable webhooks.
</Info>

## Base URLs

| Environment | URL                                 |
| ----------- | ----------------------------------- |
| Testnet     | `https://api.sandbox.immutable.com` |
| Mainnet     | `https://api.immutable.com`         |

## Next Steps

<CardGroup cols={2}>
  <Card title="Metadata Search" icon="magnifying-glass" href="/docs/products/indexer/metadata-search">
    Filter NFTs by attributes
  </Card>

  <Card title="Build a Marketplace" icon="store" href="/docs/guides/build-a-marketplace">
    Use the Indexer in your marketplace
  </Card>

  <Card title="Orderbook" icon="book" href="/docs/products/orderbook/overview">
    Build trading functionality
  </Card>

  <Card title="Contracts" icon="file-contract" href="/docs/products/asset-contracts/overview">
    Understand NFT contract structure
  </Card>
</CardGroup>
