Call uri(1) on an ERC-1155 contract and you might get back a fully resolved link, or you might get back a template containing the literal string {id} that your code is expected to replace. Both are correct behavior under the standard. That ambiguity, plus a total supply function that half of ERC-1155 contracts simply don’t have, is why “read an ERC-1155 token” trips up more developers than the equivalent ERC-721 call.
ERC-1155 metadata key facts
uri(uint256 id)is one function that serves every token ID on the contract. ERC-721’stokenURI(uint256)returns a distinct value per call; ERC-1155 usually returns the same templated string for every ID and expects the client to substitute in the ID.- The
{id}placeholder, when present, must be replaced with the token ID in lowercase hexadecimal, zero-padded to 64 characters, with no0xprefix. totalSupply(uint256)is not part of the ERC-1155 standard interface. It comes from OpenZeppelin’s optionalERC1155Supplyextension, so calling it on a contract that doesn’t inherit that extension reverts, not returns zero.- The core ERC-1155 methods share the same function selectors on every conforming contract, so one generic ERC-1155 ABI decodes
uri(),balanceOf(), and the rest anywhere. Extension methods liketotalSupply()need their own ABI fragment.
TL;DR
ERC-1155’s uri() is a single function shared by every token ID; when its return value contains {id}, replace it with the lowercase hex token ID padded to 64 characters. totalSupply(uint256) is an optional OpenZeppelin extension, not part of the base standard, so plenty of contracts don’t have it.
Why one uri() function serves every token ID
ERC-721 gives every token its own tokenURI(uint256 tokenId) call, and the contract is expected to return an already-resolved, individual URL for that ID. ERC-1155 doesn’t work that way. The EIP-1155 spec defines exactly one function, uri(uint256 id), that has to answer for every token ID the contract will ever mint, which for a game-item or trading-card contract can be thousands or millions of distinct IDs.
Storing a separate, unique string per ID on-chain for that many tokens would be expensive to write and awkward to update. So the standard allows (and most implementations use) one of two shortcuts:
- Return the exact same string for every ID, and let the metadata JSON itself carry per-ID differences, or
- Return a URI template containing the literal substring
{id}, and let the client fill in the specific token ID before fetching it.
Neither approach is “more correct” than the other; both are valid ERC-1155 contracts. A contract can also skip the template entirely and just concatenate the raw ID into the string server-side inside uri(), which is what plenty of real deployments do. As a concrete example, the demo collection on evmquery’s ERC-1155 Inspector returns ipfs://QmdEWNzkWQhvJp6AMs5iMkZ3xX3idxTp6Ai2mKwYCFWaSs/1 for token ID 1: the raw decimal ID appended directly, no {id} placeholder, no substitution needed. Your code has to handle both cases, because you can’t know which one a given contract chose without calling uri() and checking.
The {id} substitution rule, worked
When {id} does show up in the returned string, the spec is exact about the replacement: hexadecimal, lowercase, no 0x prefix, left-padded with zeroes to exactly 64 characters (32 bytes). This is the rule most implementations get subtly wrong, usually by forgetting the padding or leaving the 0x on.
Take token ID 1. Converted to hex that’s just 1, one character. Padded to 64 characters it becomes:
before: 1after: 0000000000000000000000000000000000000000000000000000000000000001So a template of https://token-cdn-domain/{id}.json resolves to https://token-cdn-domain/0000000000000000000000000000000000000000000000000000000000000001.json for token ID 1. The EIP text itself uses a less trivial example worth checking your own implementation against: token ID 314592 (0x4cce0 in hex) against the same template resolves to:
https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.jsonBoth examples are exactly 64 hex characters after the domain and before .json. If your substitution code produces a shorter string, or a string with 0x still attached, or mixed-case hex, the resulting URL is wrong and will most likely 404 against a gateway that expects the padded form.
Off-chain metadata (name, image, description) is never enforced by the contract itself. uri() only tells you where to look; nothing on-chain guarantees the JSON at that address actually matches the schema in the next section, or that it exists at all.
The ERC-1155 Metadata URI JSON Schema
Whatever the resolved URL points to is expected to conform to the schema the EIP defines, called the “ERC-1155 Metadata URI JSON Schema.” It’s a documentation-level contract, not something enforced on-chain, and every field in it is optional:
| Field | Type | Meaning |
|---|---|---|
name |
string | Identifies the asset the token represents. |
decimals |
integer | Number of decimal places to display, for tokens meant to be shown as fractional amounts (a fungible in-game currency, for example). Defaults to 0, meaning the token displays as a whole number. |
description |
string | Human-readable description of the asset. |
image |
string (URI) | Points to a resource with MIME type image/*, ideally 1:1 aspect ratio, that represents the asset. |
properties |
object | Arbitrary key/value pairs. Values may be strings, numbers, objects, or arrays; the schema doesn’t constrain what goes in here. |
That’s the full set. There’s no attributes array standardized the way marketplaces later converged on for ERC-721 collections; anything beyond name, decimals, description, image, and properties is a de facto convention some marketplace adopted, not part of EIP-1155 itself. If you’re building a reader that has to work across arbitrary ERC-1155 collections, treat every field as optionally absent and don’t assume a marketplace-specific extension will be there.
Reading total supply (and why plenty of contracts don’t have it)
This is the most common surprise for developers coming from ERC-721, where enumeration and supply tracking are common add-ons. ERC-1155’s mandatory interface is limited to safeTransferFrom, safeBatchTransferFrom, balanceOf, balanceOfBatch, setApprovalForAll, and isApprovedForAll, plus the uri() read covered above. There is no totalSupply in that list. It was never part of the standard.
totalSupply(uint256 id) exists because OpenZeppelin ships it as an optional extension, ERC1155Supply, that a project’s contract has to explicitly inherit. The extension adds totalSupply(uint256 id) (how many of that specific ID have been minted, net of burns) and exists(uint256 id) (whether that ID has ever been minted). A contract that doesn’t inherit ERC1155Supply simply doesn’t have this function at all: the call doesn’t revert with “zero supply,” it reverts because there’s no matching function selector on the deployed bytecode. If you’re scripting reads across a batch of collections, expect a meaningful fraction of them to fail on totalSupply specifically, independent of whether the contract or the token ID is otherwise valid.
Practically, that means: check whether a contract implements the extension before you build a query that depends on it, and don’t treat a revert on totalSupply(id) as evidence the token doesn’t exist. It might exist fine; the contract just never opted into supply tracking.
Where the ABI for these functions comes from
Because uri(), balanceOf(), safeTransferFrom(), and the rest of the core interface are defined by the standard itself, their function selectors are identical on every conforming ERC-1155 contract. That means a single, generic ERC-1155 ABI JSON, whether you copy it from the EIP text, pull it from OpenZeppelin’s compiled build artifacts, or import it from a package like viem’s built-in ABI helpers, decodes those core methods against any ERC-1155 contract regardless of who deployed it or what else the contract does.
The optional extension methods break that guarantee. totalSupply(uint256) isn’t part of the fixed interface, so it isn’t in a generic ERC-1155 ABI; you either need the ERC1155Supply extension’s own ABI fragment, or you need to confirm the specific contract’s verified source before assuming the function is there.
evmquery skips the manual part of this entirely. Point it at a contract address and it pulls the verified source for that exact deployment (resolving through an EIP-1967 proxy first if there is one), reconstructs the real interface, and exposes uri() and totalSupply() as callable methods only when the deployed contract actually has them. There’s no static ABI file to keep in sync and no guessing whether a given collection implements the supply extension.
{ "uri": "token.uri(id)", "totalSupply": "string(formatUnits(token.totalSupply(id), 0))"}That expression, run through the ERC-1155 Inspector, returns both fields (or just the URI, if the contract skips supply tracking) in one Multicall3 round trip. The same query is available from any language your stack already uses through the REST API, no proxy resolution or ABI management required on your end.
Next steps
- ERC-1155 Inspector: run
uri(id)andtotalSupply(id)against a live contract and see the{id}substitution applied automatically. - Fixing “could not decode result data (value=“0x”)“ in Ethers and Viem: what actually happens when you call a function, like
totalSupply, that the deployed contract doesn’t implement. - evmquery for developers: the full integration story for REST, MCP, and n8n.
- Multicall3 batching for EVM contract reads: why
uri()andtotalSupply()above cost one round trip instead of two.



