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
| 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 |
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;
}
Use webhooks when:
- Syncing to your database
- Triggering game events on sales/transfers
- Real-time leaderboards or activity feeds
Configure webhooks in Hub.| 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 |
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 })
);
}
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');
}
| Tier | Rate Limit |
|---|
| Standard | 50 req/sec |
| Enterprise | Custom |
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
| Environment | URL |
|---|
| Testnet | https://api.sandbox.immutable.com |
| Mainnet | https://api.immutable.com |
Next Steps