NFT metadata
Fetch NFT metadata, list collection contents, and look up current owners
Four endpoints for ERC-721 and ERC-1155 collections: per-token metadata, paginated collection listings, current owner lookup, and the inverse - all NFTs held by an account.
All four are read-only and use the standard api-key header (see API Keys).
GET /api/nft/metadata/ - one NFT
Returns metadata for a specific NFT. Cache-first: if we already fetched it we serve the cached row. Otherwise we call token_uri / tokenURI on the contract, parse the response (handles both Cairo 1 ByteArray and legacy Cairo 0 felt arrays), follow the URI to its JSON body, and cache the result.
| Parameter | Required |
|---|---|
token_address | yes - the NFT contract address |
nft_token_id | yes - the specific NFT id within the collection |
1curl 'https://api.tokenkithq.io/api/nft/metadata/?token_address=0x020c9638...&nft_token_id=430' \2 -H 'api-key: YOUR_API_KEY'1{2 "success": true,3 "data": {4 "token_address": "0x020c9638...",5 "nft_token_id": "430",6 "name": "Doodle #430",7 "description": "A community-driven collectibles project...",8 "image": "https://ipfs.io/ipfs/QmX3XNKi8s.../430.png",9 "external_url": null,10 "animation_url": null,11 "attributes": [12 { "trait_type": "face", "value": "content" },13 { "trait_type": "hair", "value": "blue alfalfa" }14 ],15 "metadata_uri": "https://api.comoco.xyz/metadata/0x020c9638.../430"16 }17}image is already resolved from ipfs:// to a usable HTTPS gateway URL. metadata_uri is the raw URI returned by the contract - useful if you want to fetch the source JSON yourself.
If the source contract reverts (token id never minted, broken metadata logic), you get a 404 with the on-chain reason in detail.
GET /api/nft/collection/ - NFTs in one collection
Paginated list of NFTs we have cached metadata for. This pulls from cached rows only - it does not fetch missing NFTs on the fly. Use /api/nft/metadata/ for that.
| Parameter | Required | Notes |
|---|---|---|
token_address | yes | The collection's contract address |
page | no | Default 1 |
page_size | no | Default 20, max 100 |
1curl 'https://api.tokenkithq.io/api/nft/collection/?token_address=0x020c9638...&page=1&page_size=20' \2 -H 'api-key: YOUR_API_KEY'1{2 "success": true,3 "token_address": "0x020c9638...",4 "page": 1,5 "page_size": 20,6 "count": 247,7 "data": [8 {9 "nft_token_id": "1",10 "name": "Doodle #1",11 "description": "...",12 "image": "https://ipfs.io/ipfs/.../1.png",13 "external_url": null,14 "animation_url": null,15 "attributes": [...],16 "metadata_uri": "https://..."17 }18 ]19}GET /api/nft/owner/ - current owner of one NFT
Returns the address that currently holds a specific NFT. Derived from the on-chain transfer log (most recent recipient wins).
| Parameter | Required |
|---|---|
token_address | yes |
nft_token_id | yes |
1curl 'https://api.tokenkithq.io/api/nft/owner/?token_address=0x020c9638...&nft_token_id=430' \2 -H 'api-key: YOUR_API_KEY'1{2 "success": true,3 "data": {4 "token_address": "0x020c9638...",5 "nft_token_id": "430",6 "owner_address": "0x05d0f2e1fceecd2c890c64fb9bef1804a8af31e8a259fdfdc0994e6f5fd942a5",7 "last_transfer_block": 7888,8 "last_transfer_timestamp": "2022-11-05T07:34:07+01:00",9 "last_transfer_txhash": "0x01ce26493a91ba6440f43e9e1d1011ee6ae3be432521a3828f07ad52fb861237"10 }11}Returns 404 if no transfers exist for that NFT (it was probably never minted).
For ERC-1155 collections this only tells you the most recent recipient, not all current holders - quantities can be split across many addresses. Use the transfers endpoint for the full picture.
GET /api/nft/owned-by/ - all NFTs an account holds
Returns every NFT currently held by a given account, across every NFT collection (or scoped to one if you pass token_address). Always carries the parent collection's name/symbol/logo; optionally merges in cached per-NFT metadata.
| Parameter | Required | Notes |
|---|---|---|
account_address | yes | The wallet whose NFTs you want |
token_address | no | Limit to one collection |
page | no | Default 1 |
page_size | no | Default 20, max 100 |
include_metadata | no | true (default) merges in per-NFT name/image/attributes; false returns just the ownership + collection identity |
1curl 'https://api.tokenkithq.io/api/nft/owned-by/?account_address=0x05d0f2e1...&page_size=20' \2 -H 'api-key: YOUR_API_KEY'Response shape
Every entry in data always includes the same eight identity fields. When include_metadata=true (default) and we have a cached is_fetched=True row for that NFT, five extra fields are merged in.
| Field | Type | Always present? | Source |
|---|---|---|---|
token_address | string | yes | ClickHouse transfer log |
nft_token_id | string | yes | ClickHouse transfer log |
last_transfer_block | number | yes | ClickHouse transfer log |
collection_name | string | null | yes | Token.name |
collection_symbol | string | null | yes | Token.symbol |
collection_logo | string | null | yes | Token.logo |
is_erc721 | boolean | yes | Token.is_erc721 |
is_erc1155 | boolean | yes | Token.is_erc1155 |
name | string | null | only if metadata cached | NftMetadata.name |
image | string | null | only if metadata cached | NftMetadata.image (gateway-resolved) |
description | string | null | only if metadata cached | NftMetadata.description |
attributes | array | only if metadata cached | NftMetadata.attributes |
metadata_uri | string | null | only if metadata cached | raw URI returned by the contract |
The identity fields let a wallet render a useful card ("Thomas Sinclair and Son ยท #3") even before per-NFT metadata has been fetched. The metadata fields fill in the picture name and image when available.
ERC-1155 caveat. For ERC-1155 collections the response identifies "the most recent recipient" of each (token, nft_token_id) โ it does not report per-holder balances. A 1155 token id can be held by many wallets simultaneously, each with a different quantity, and this endpoint can't currently represent that fan-out. Use is_erc1155: true to switch the wallet UX (don't show "you own this NFT" โ show "you received N at block X") until the quantity-correct endpoint lands.
Example - per-NFT metadata cached
1{2 "success": true,3 "page": 1,4 "page_size": 20,5 "count": 1,6 "data": [7 {8 "token_address": "0x020c9638...",9 "nft_token_id": "430",10 "last_transfer_block": 7888,11 "collection_name": "Doodles",12 "collection_symbol": "DOOD",13 "collection_logo": "https://.../doodles-logo.png",14 "is_erc721": true,15 "is_erc1155": false,16 "name": "Doodle #430",17 "image": "https://ipfs.io/ipfs/.../430.png",18 "description": "A community-driven collectibles project...",19 "attributes": [20 { "trait_type": "face", "value": "content" }21 ],22 "metadata_uri": "https://api.comoco.xyz/metadata/0x020c9638.../430"23 }24 ]25}Example - per-NFT metadata not cached yet
The name / image / description / attributes / metadata_uri keys are simply absent. Collection identity is still there.
1{2 "success": true,3 "page": 1,4 "page_size": 100,5 "count": 2,6 "data": [7 {8 "token_address": "0x0111e08c883b8387e7ecbe2b4b6a502a294f7443d1cc8075df34130439f5f0cf",9 "nft_token_id": "3",10 "last_transfer_block": 10023047,11 "collection_name": "Thomas Sinclair and Son",12 "collection_symbol": "TSAS",13 "collection_logo": null,14 "is_erc721": true,15 "is_erc1155": false16 },17 {18 "token_address": "0x06ad4d55ed2d12ad5d6d55587d800874ef807b051bfefe13497e60ec5b019369",19 "nft_token_id": "26",20 "last_transfer_block": 10022994,21 "collection_name": "MediolanoIntelctualProperty",22 "collection_symbol": "MIP",23 "collection_logo": null,24 "is_erc721": true,25 "is_erc1155": false26 }27 ]28}A wallet that wants the full picture can either (a) call /api/nft/metadata/ per missing NFT to trigger an on-demand fetch + cache, or (b) just render with what's there - the background warmer will eventually fill the cache (see How metadata caching works).
How metadata caching works
We fetch metadata three ways:
- On-transfer. Whenever a transfer of an NFT lands in the indexer, we queue a one-shot fetch for that specific
(token, nft_token_id). Most NFTs people actually move get cached automatically within seconds. - On-demand. First call to
/api/nft/metadata/for an uncached NFT triggers a synchronous fetch. Subsequent calls are cache hits. - Periodic sweeps. A 5-minute background task and an hourly catch-up pick up any NFTs that were missed (e.g. seen in transfers before the contract was classified as ERC-721, or contracts whose metadata host was offline at indexing time).
Retry policy for failed fetches
Failed fetches are bounded so one broken metadata host can't monopolize the warmer budget:
- Every failure increments an
attempt_countand stampslast_attempt_at. - Retries are spaced out by a backoff curve - failed rows are skipped until that window elapses.
- After 10 failed attempts an
(token, nft_token_id)is considered permanently broken and the background sweepers stop touching it. A successful fetch at any point resets the counter to 0.
Practical consequences for API consumers:
- An NFT whose contract returns an offline URI may show up in
/api/nft/owned-by/without per-NFT metadata for a while. The collection identity (collection_name/collection_symbol) is still there immediately. /api/nft/metadata/triggers a fetch on cache miss, but a row that's still inside the backoff window or already past the 10-attempt cap is treated as "not ready yet" - the endpoint returns a 404 without re-hitting the failing host. This is intentional: it stops a hammering wallet from re-amplifying the load on a broken metadata server. When the host comes back, the next scheduled retry caches the row and subsequent calls succeed.
Contract-side URI repair
A handful of contracts store malformed URIs (e.g. https://ipfs.io/ipfs/ipfs://bafkrei... - a gateway URL prepended over an ipfs:// URI). The resolver strips the duplicated ipfs:// and resolves the underlying CID, so these collections still cache correctly without contract changes.
Dynamic metadata
If the source contract returns dynamic metadata (soulbound tokens that mutate, NFTs whose URI flips after reveal), the cached value can lag the on-chain state. We do not currently expose a force-refresh on the API; ask the team if you have a use case.
See also
- API Keys - how to authenticate
- Querying transfers - the raw transfer log NFT ownership is derived from