Metadata Standards
How to add rich metadata to your ERC721 or ERC1155 NFTs
Providing asset metadata allows applications like OpenSea to pull in rich data for digital assets. Digital assets on a smart contract are typically represented by a unique identifier (e.g., the tokenId
in ERC721), so metadata allows these assets to have additional properties, such as a name, description, and image.
Implementing token URI
For OpenSea to pull metadata for ERC721 and ERC1155 assets, your contract will need to return a URI where we can find the metadata or return them encoded onchain. To find this URI, we use the tokenURI
method in ERC721 and the uri
method in ERC1155.
/**
* @dev Returns the metadata URI for a given token ID.
*/
function tokenURI(uint256 _tokenId) public view returns (string) {
return string.concat(
baseTokenURI(),
Strings.uint2str(_tokenId)
);
}
The tokenURI
function in your ERC721 or the uri
function in your ERC1155 contract should return an HTTP or IPFS URL. When queried, this URL should return a JSON blob of data with the metadata for your token.
See the section on IPFS and Arweave below for how to handle decentralized metadata URIs, as well as Onchain Metadata to learn more about how to return those.
Metadata structure
OpenSea supports metadata that is structured according to the official ERC721 metadata standard or the ERC1155 metadata standard.

Additionally, we support several other properties that allow for multimedia attachments -- including audio, video, and 3D models -- plus interactive traits for your items, giving you all the sorting and filtering capabilities on the OpenSea marketplace.
Here's an example of metadata for one of the OpenSea creatures:
{
"description": "Friendly OpenSea Creature that enjoys long swims in the ocean.",
"external_url": "https://openseacreatures.io/3",
"image": "https://storage.googleapis.com/opensea-prod.appspot.com/puffs/3.png",
"name": "Dave Starbelly",
"attributes": [ ... ]
}
Here's how each of these properties work:
name | Name of the item. |
description | A human-readable description of the item. Markdown is supported. |
image | This is the URL to the image of the item. Can be just about any type of image (including SVGs, which will be cached into PNGs by OpenSea), IPFS or Arweave URLs or paths. We recommend using a minimum 3000 x 3000 image. |
animation_url | A URL to a multi-media attachment for the item. The file extensions GLTF, GLB, WEBM, MP4, M4V, OGV, and OGG are supported, along with the audio-only extensions MP3, WAV, and OGA. Animation_url also supports HTML pages, allowing you to build rich experiences and interactive NFTs using JavaScript canvas, WebGL, and more. Scripts and relative paths within the HTML page are now supported. However, access to browser extensions is not supported. |
attributes | These are the attributes for the item, which will show up on the OpenSea page for the item. (see below) |
background_color | Background color of the item on OpenSea. Must be a six-character hexadecimal without a pre-pended #. |
external_url | This is the URL that will appear on OpenSea for the asset and will allow users to leave OpenSea and view the item on your site. |
If you host your media off of OpenSea, the maximum file size for media supported is 300MB. However we recommend keeping it under 200MB for faster load times.
Attributes
To give your items a little more pizazz, we also allow you to add custom "attributes" to your metadata that will show up underneath each of your assets.
To generate those attributes, the following array of attributes was included in the metadata:
...
{
"attributes": [
{
"trait_type": "Base",
"value": "Starfish"
},
{
"trait_type": "Eyes",
"value": "Big"
},
{
"trait_type": "Mouth",
"value": "Surprised"
},
{
"trait_type": "Level",
"value": 5
},
{
"trait_type": "Stamina",
"value": 1.4
},
{
"trait_type": "Personality",
"value": "Sad"
},
{
"display_type": "boost_number",
"trait_type": "Aqua Power",
"value": 40
},
{
"display_type": "boost_percentage",
"trait_type": "Stamina Increase",
"value": 10
},
{
"display_type": "number",
"trait_type": "Generation",
"value": 2
}
]
}
Here trait_type
is the name of the trait, value
is the value of the trait, and display_type
is a field indicating how you would like it to be displayed. For string
traits, you don't have to worry about display_type
.
Numeric traits
For numeric traits, OpenSea supports number
. Adding an optional max_value
sets a ceiling for a numerical trait's possible values. It defaults to the maximum that OpenSea has seen so far on the assets on your contract. If you set a max_value
, make sure not to pass in a higher value
.
Date traits
OpenSea also supports a date
display_type
.
{
"display_type": "date",
"trait_type": "birthday",
"value": 1546360800
}
If you don't want to have a trait_type
for a particular trait, you can include just a value in the trait and it will be set as a generic string property. For example,
{
"value": "Happy"
}
Attribute guidelines
A couple of important notes when coming up with your attributes! You should include string attributes as strings (remember the quotes), and numeric properties as either floats or integers so that OpenSea can display them appropriately. String properties should be human-readable strings.
Your metadata API
You're welcome to deploy your metadata API however you see fit: you can host it on IPFS, cloud storage, or your own servers. You can also return onchain metadata.
IPFS and Arweave URIs
OpenSea supports the storage of NFT metadata in decentralized file networks, so that they can’t be modified by a central party.
If you use IPFS to host your metadata, your URL should be in the format ipfs://<hash>
. For example, ipfs://QmTy8w65yBXgyfG2ZBg5TrfB2hPjrDQH3RCQFJGkARStJb
. If you plan to store on IPFS, we recommend Pinata for easily storing data.
Arweave's equivalent is ar://<hash>
. For example, ar://jK9sR4OrYvODj7PD3czIAyNJalub0-vdV_JAg1NqQ-o
.
Onchain Metadata
Instead of hosting metadata on IPFS or a centralized server, you can embed it directly onchain. This is often used for small collections, fully self-contained NFTs, or generative art projects that don't depend on off-chain infrastructure.
In this approach, your contract's tokenURI
(ERC721) or uri
(ERC1155) function returns a Base64-encoded JSON string, prefixed with the MIME type data:application/json;base64,
. This allows websites like OpenSea to decode and render the metadata directly from the blockchain without making an external request.
Here’s a minimal example implementation:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract OnchainMetadataExample is ERC721 {
constructor() ERC721("OnchainCreature", "OCC") {
_mint(msg.sender, 1);
}
function tokenURI(uint256 tokenId) public pure override returns (string memory) {
require(tokenId == 1, "Nonexistent token");
// Create JSON metadata
string memory json = Base64.encode(
bytes(
string.concat(
'{"name":"Onchain Creature #1",',
'"description":"A fully onchain NFT stored in Base64 JSON.",',
'"image":"data:image/svg+xml;base64,',
Base64.encode(bytes('<svg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 100 100\'><circle cx=\'50\' cy=\'50\' r=\'40\' fill=\'aqua\' /></svg>')),
'"}'
)
)
);
// Return encoded data URI
return string.concat("data:application/json;base64,", json);
}
}
However, keep in mind that onchain metadata increases gas costs, so it’s best suited for small or generative NFTs where metadata is compact.
Metadata updates
To refresh token metadata on OpenSea, you can emit on-chain events as defined in ERC-4906:
event MetadataUpdate(uint256 _tokenId);
event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId);
To refresh a whole collection, emit _toTokenId
with type(uint256).max
For ERC1155, metadata updates are supported via the specification for the event URI
:
event URI(string _value, uint256 indexed _id);
Alternatively, you can hit the OpenSea API: https://docs.opensea.io/reference/refresh_nft
Disable trading for staked or locked tokens
OpenSea supports events to signal that a token should not be eligible for trading. This helps prevent "execution reverted" errors for your users if transfers are disabled while in a staked or locked state.
These events are:
// ERC-5192 (recommended for gas efficiency)
event Locked(uint256 tokenId);
event Unlocked(uint256 tokenId);
// ERC-5753
event Lock(address indexed unlocker, uint256 indexed id);
event Unlock(uint256 indexed id);
// Others
event TokenLocked(uint256 indexed tokenId, address indexed approvedContract);
event TokenUnlocked(uint256 indexed tokenId, address indexed approvedContract);
event Stake(uint256 indexed tokenId);
event Unstake(uint256 indexed tokenId, uint256 stakedAtTimestamp, uint256 removedFromStakeAtTimestamp);
event Staked(address indexed user, uint256[] tokenIds, uint256 stakeTime);
event Unstaked(address indexed user, uint256[] tokenIds);
OpenSea only uses the tokenId
event parameter to enable or disable trading and does not use any of the other parameters.
Updated 6 days ago