List a Hedera account's NFTs
Read an account's NFT holdings through the Hedera mirror API. Follow pagination, keep requests bounded and distinguish unavailable data from an empty wallet.
Content reviewed on
The request
GET /api/v1/accounts/{accountId}/nfts?limit=100nfts and a pagination link in links.next. Continue through those links until the result ends, or until your application's request budget is reached.
A bounded Node example
list-nfts.mjs. Run it with a real mainnet account ID, for example node list-nfts.mjs YOUR_ACCOUNT_ID. The script requires a supported Node runtime with built-in fetch.
const account = process.argv[2];
if (!/^\d+\.\d+\.\d+$/.test(account ?? '')) {
throw new Error('Pass a Hedera account ID such as shard.realm.number.');
}
const origin = 'https://mainnet.mirrornode.hedera.com';
const path = `/api/v1/accounts/${account}/nfts`;
let next = new URL(`${path}?limit=100`, origin);
const visited = new Set();
const rows = new Map();
let complete = false;
for (let page = 0; page < 10 && next; page += 1) {
if (next.origin !== origin || next.pathname !== path ||
next.username || next.password || next.hash) {
throw new Error('Unexpected pagination URL.');
}
if (visited.has(next.href)) throw new Error('Repeated pagination URL.');
visited.add(next.href);
const response = await fetch(next, {
signal: AbortSignal.timeout(10000),
redirect: 'error'
});
if (!response.ok) {
throw new Error(`Mirror HTTP ${response.status}; stop and check the provider.`);
}
const body = await response.json();
if (!Array.isArray(body.nfts) || !body.links ||
!Object.hasOwn(body.links, 'next')) {
throw new Error('Unexpected mirror response.');
}
for (const nft of body.nfts) {
if (!nft || typeof nft.token_id !== 'string' ||
!Number.isSafeInteger(nft.serial_number) || nft.serial_number < 1) {
throw new Error('Unexpected NFT identifier.');
}
rows.set(`${nft.token_id}/${nft.serial_number}`, {
tokenId: nft.token_id,
serial: nft.serial_number
});
}
const link = body.links.next;
if (link === null) {
complete = true;
next = null;
} else {
if (typeof link !== 'string' || link.length === 0) {
throw new Error('Unexpected next link.');
}
next = new URL(link, origin);
}
}
console.log(JSON.stringify({
network: 'mainnet',
account,
pages: visited.size,
complete,
nfts: [...rows.values()]
}, null, 2));complete is false, you have a partial result. Do not label it as the account's entire portfolio.


