Mint NFTs at scale without managing nonces, batching, gas, or indexing—everything is handled through a single API call.
Why Use the Minting API?
Feature Benefit Managed Infrastructure No nonce tracking, transaction lifecycle, or gas management Batch Optimization Multiple mint requests combined into gas-efficient transactions Metadata Indexing Metadata indexed immediately without crawling your baseURI Rate Limit Handling Built-in queuing with clear rate limit feedback Idempotent Requests Safe to retry with same reference_id
The Minting API is a REST API. See the endpoint examples below for request/response formats.
Prerequisites
Security: Keep your Secret API Key server-side only. Never expose it in client code. See API Keys Security for best practices.
Rate Limits
Tier NFTs/SFTs per minute Burst Use Case Standard 200 2,000 Testing, small mints Partner 2,000 20,000 Production games Enterprise Custom Custom High-volume launches
Rate limits count distinct token_ids, not total tokens. Minting 1000 copies of the same ERC-1155 token_id counts as 1 request.
Rate limit headers in response:
{
"imx_mint_requests_limit" : "2000" ,
"imx_remaining_mint_requests" : "1999" ,
"imx_mint_requests_limit_reset" : "2024-02-13 07:20:00 UTC" ,
"imx_mint_requests_retry_after" : "59.98-seconds"
}
Minting ERC-721
With Token ID (mintBatch)
Use when you need specific token_ids (e.g., migrating existing assets):
// Types
interface MintMetadata {
name : string ;
description ?: string ;
image : string ;
external_url ?: string ;
animation_url ?: string ;
attributes ?: Array <{
trait_type : string ;
value : string | number ;
display_type ?: 'number' | 'boost_percentage' | 'boost_number' | 'date' ;
}>;
}
interface MintAsset {
reference_id : string ;
owner_address : string ;
token_id ?: string ;
metadata ?: MintMetadata ;
}
const BASE_URL = 'https://api.sandbox.immutable.com' ; // or api.immutable.com for mainnet
const CHAIN_NAME = 'imtbl-zkevm-testnet' ; // or imtbl-zkevm-mainnet
// Mint ERC-721 with token_id specified
export async function mintERC721WithTokenId (
contractAddress : string ,
assets : MintAsset [],
secretApiKey : string
) {
const response = await fetch (
` ${ BASE_URL } /v1/chains/ ${ CHAIN_NAME } /collections/ ${ contractAddress } /nfts/mint-requests` ,
{
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'x-immutable-api-key' : secretApiKey ,
},
body: JSON . stringify ({ assets }),
}
);
if ( ! response . ok ) {
const error = await response . json ();
throw new Error ( `Mint failed: ${ error . message } ` );
}
return response . json ();
}
// Example: Mint a single NFT with metadata
export async function mintSingleNFT (
contractAddress : string ,
recipientAddress : string ,
tokenId : string ,
metadata : MintMetadata ,
secretApiKey : string
) {
const assets : MintAsset [] = [
{
reference_id: tokenId ,
owner_address: recipientAddress ,
token_id: tokenId ,
metadata ,
},
];
return mintERC721WithTokenId ( contractAddress , assets , secretApiKey );
}
Without Token ID (mintBatchByQuantity)
More gas-efficient. System generates sequential token_ids:
const response = await fetch (
`https://api.immutable.com/v1/chains/imtbl-zkevm-mainnet/collections/ ${ contractAddress } /nfts/mint-requests` ,
{
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'x-immutable-api-key' : secretApiKey ,
},
body: JSON . stringify ({
assets: [
{
reference_id: 'reward-123' , // Your internal ID
owner_address: playerAddress ,
// No token_id - system assigns one
metadata: {
name: 'Daily Reward' ,
image: 'https://assets.example.com/reward.png' ,
},
},
],
}),
}
);
Use the reference_id to look up the assigned token_id after minting completes.
mintBatchByQuantity may return incomplete balanceOf() results for collections with 80,000+ tokens. Contact Immutable if you need both features.
Minting ERC-1155
interface MintAssetERC1155 {
reference_id : string ;
owner_address : string ;
token_id : string ;
amount : string ;
metadata ?: {
name : string ;
description ?: string ;
image : string ;
attributes ?: Array <{ trait_type : string ; value : string | number }>;
};
}
const BASE_URL = 'https://api.sandbox.immutable.com' ;
const CHAIN_NAME = 'imtbl-zkevm-testnet' ;
// Mint ERC-1155 tokens
export async function mintERC1155 (
contractAddress : string ,
assets : MintAssetERC1155 [],
secretApiKey : string
) {
const response = await fetch (
` ${ BASE_URL } /v1/chains/ ${ CHAIN_NAME } /collections/ ${ contractAddress } /nfts/mint-requests` ,
{
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'x-immutable-api-key' : secretApiKey ,
},
body: JSON . stringify ({ assets }),
}
);
if ( ! response . ok ) {
const error = await response . json ();
throw new Error ( `Mint failed: ${ error . message } ` );
}
return response . json ();
}
// Example: Mint starter pack with multiple item types
export async function mintStarterPack (
contractAddress : string ,
recipientAddress : string ,
secretApiKey : string
) {
const starterPackItems : MintAssetERC1155 [] = [
{
reference_id: `starter-sword- ${ Date . now () } ` ,
owner_address: recipientAddress ,
token_id: '1' , // Sword
amount: '1' ,
metadata: {
name: 'Iron Sword' ,
image: 'https://example.com/sword.png' ,
attributes: [{ trait_type: 'Attack' , value: 10 }],
},
},
{
reference_id: `starter-potion- ${ Date . now () } ` ,
owner_address: recipientAddress ,
token_id: '2' , // Health Potion
amount: '5' , // Give 5 potions
metadata: {
name: 'Health Potion' ,
image: 'https://example.com/potion.png' ,
},
},
];
return mintERC1155 ( contractAddress , starterPackItems , secretApiKey );
}
Key Differences from ERC-721
Aspect ERC-1155 token_idAlways required amountRequired (number of tokens) Metadata Only on first mint for a token_id Duplicate token_id Allowed (mints additional quantity)
Passing metadata when minting to an existing token_id returns 409 Conflict. Check if the token exists before including metadata.
Tracking Mint Status
interface MintRequestStatus {
chain : { id : string ; name : string };
collection_address : string ;
reference_id : string ;
status : 'pending' | 'succeeded' | 'failed' ;
token_id : string | null ;
owner_address : string ;
transaction_hash : string | null ;
error : string | null ;
}
const BASE_URL = 'https://api.sandbox.immutable.com' ;
const CHAIN_NAME = 'imtbl-zkevm-testnet' ;
// Get mint request status by reference_id
export async function getMintStatus (
contractAddress : string ,
referenceId : string ,
secretApiKey : string
) : Promise < MintRequestStatus > {
const response = await fetch (
` ${ BASE_URL } /v1/chains/ ${ CHAIN_NAME } /collections/ ${ contractAddress } /nfts/mint-requests/ ${ referenceId } ` ,
{
headers: { 'x-immutable-api-key' : secretApiKey },
}
);
if ( ! response . ok ) {
const error = await response . json ();
throw new Error ( `Status check failed: ${ error . message } ` );
}
const data = await response . json ();
return data . result [ 0 ];
}
// Poll for mint completion with timeout
export async function waitForMint (
contractAddress : string ,
referenceId : string ,
secretApiKey : string ,
maxAttempts = 30 ,
intervalMs = 2000
) : Promise < MintRequestStatus > {
for ( let i = 0 ; i < maxAttempts ; i ++ ) {
const status = await getMintStatus ( contractAddress , referenceId , secretApiKey );
if ( status . status === 'succeeded' ) {
console . log ( `Mint succeeded! Token ID: ${ status . token_id } ` );
return status ;
}
if ( status . status === 'failed' ) {
throw new Error ( `Mint failed: ${ status . error } ` );
}
await new Promise (( resolve ) => setTimeout ( resolve , intervalMs ));
}
throw new Error ( `Mint timed out after ${ maxAttempts } attempts` );
}
Status Values
Status Meaning pendingRequest queued, waiting for blockchain confirmation succeededMint completed, token_id and transaction_hash available failedMint failed, see error field
Webhooks (Recommended)
Configure webhooks in Hub to receive real-time updates:
// Webhook payload for imtbl_zkevm_mint_request_updated
{
event_name : 'imtbl_zkevm_mint_request_updated' ,
data : {
reference_id : 'reward-123' ,
status : 'succeeded' ,
token_id : '456' ,
transaction_hash : '0x...' ,
collection_address : '0x...' ,
owner_address : '0x...' ,
}
}
Include with Mint Request (Recommended)
Include metadata in the mint request for immediate indexing:
{
assets : [{
reference_id: '123' ,
owner_address: playerAddress ,
token_id: '123' ,
metadata: {
name: 'Legendary Sword' ,
description: 'A powerful weapon' ,
image: 'https://assets.example.com/sword.png' ,
animation_url: 'https://assets.example.com/sword.mp4' ,
attributes: [
{ trait_type: 'Rarity' , value: 'Legendary' },
{ trait_type: 'Damage' , value: 100 , display_type: 'number' },
],
},
}],
}
Host at baseURI (Required)
Even when including metadata in the API call, you must also host the metadata file at {baseURI}/{token_id}. Some ecosystem partners fetch directly from the source.
Metadata Format See the metadata format specification
When in-game assets evolve through player actions (leveling up, crafting, performance enhancements), you need to refresh the metadata so marketplaces and other ecosystem partners reflect the updated attributes.
Immutable does not listen for URI update events. You must explicitly trigger a metadata refresh for changes to propagate across the ecosystem.
Use Case Description Leveling Up Character gains experience or unlocks new abilities Crafting Items are combined or upgraded Reveals Delayed reveal for primary sales or mystery boxes Balance Updates Adjusting game stats or rarity after launch Content Fixes Correcting art, names, or descriptions
Refresh by Token ID
Push updated metadata for specific tokens:
interface RefreshMetadataRequest {
nft_metadata : Array <{
token_id : string ;
name : string ;
description ?: string ;
image : string ;
external_url ?: string ;
animation_url ?: string ;
attributes ?: Array <{
trait_type : string ;
value : string | number ;
display_type ?: 'number' | 'boost_percentage' | 'boost_number' | 'date' ;
}>;
}>;
}
const BASE_URL = 'https://api.sandbox.immutable.com' ; // or api.immutable.com for mainnet
const CHAIN_NAME = 'imtbl-zkevm-testnet' ; // or imtbl-zkevm-mainnet
export async function refreshNFTMetadata (
contractAddress : string ,
metadata : RefreshMetadataRequest ,
secretApiKey : string
) {
const response = await fetch (
` ${ BASE_URL } /v1/chains/ ${ CHAIN_NAME } /collections/ ${ contractAddress } /nfts/refresh-metadata` ,
{
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'x-immutable-api-key' : secretApiKey ,
},
body: JSON . stringify ( metadata ),
}
);
if ( ! response . ok ) {
const error = await response . json ();
throw new Error ( `Metadata refresh failed: ${ error . message } ` );
}
// Check rate limit headers
console . log ( 'Remaining refreshes:' , response . headers . get ( 'imx_remaining_refreshes' ));
console . log ( 'Limit resets at:' , response . headers . get ( 'imx_refresh_limit_reset' ));
return response . json ();
}
// Example: Update a leveled-up character
await refreshNFTMetadata (
contractAddress ,
{
nft_metadata: [
{
token_id: '123' ,
name: 'Hero - Level 5' ,
description: 'A battle-hardened warrior' ,
image: 'https://assets.example.com/hero-level5.png' ,
attributes: [
{ trait_type: 'Level' , value: 5 , display_type: 'number' },
{ trait_type: 'Attack' , value: 25 , display_type: 'number' },
{ trait_type: 'Defense' , value: 15 , display_type: 'number' },
],
},
],
},
secretApiKey
);
Submit the entire asset metadata when refreshing—not just changed attributes. Omitted fields will be removed.
Refresh Rate Limits
Resource Limit Total NFTs updated 677 per minute Token IDs per request Up to 250 Total metadata size 228 KiB per request Metadata stacks (by metadata_id) 677 per minute, up to 10 IDs per request
Rate limit response headers:
{
"imx_refreshes_limit" : "10000" ,
"imx_refresh_limit_reset" : "2024-02-13 15:04:05" ,
"imx_remaining_refreshes" : "9000" ,
"retry_after" : "2-seconds"
}
Use metadata_id for bulk updates. Refreshing by metadata_id updates all NFTs that share that metadata stack, which is significantly faster than refreshing individual tokens.
Metadata refreshes are asynchronous. Check the metadata_synced_at field via the Indexer to confirm updates:
// Query the NFT to check when metadata was last synced
const nft = await fetch (
` ${ BASE_URL } /v1/chains/ ${ CHAIN_NAME } /collections/ ${ contractAddress } /nfts/ ${ tokenId } ` ,
{ headers: { 'x-immutable-api-key' : apiKey } }
). then (( r ) => r . json ());
console . log ( 'Metadata last synced:' , nft . metadata_synced_at );
// Output: "2024-02-13T15:04:05.123Z"
Updated metadata is typically reflected on marketplaces within ~8 seconds.
Error Handling
Request Errors (No Blockchain Event)
Error Code Cause VALIDATION_ERROR400 >100 assets, duplicate reference_id, invalid format UNAUTHORISED_REQUEST401 Invalid API key AUTHENTICATION_ERROR403 Collection not linked to API key CONFLICT409 reference_id or token_id already usedTOO_MANY_REQUESTS_ERROR429 Rate limit exceeded
Transaction Errors (Check via Status API)
Error Cause Solution AccessControl: account ... is missing roleMinter role not granted Grant minter role to Immutable’s minting wallet Exceeded maximum submissionsTransaction retry failed Resubmit the request
Idempotent Requests
Safe to retry failed network requests—duplicate reference_id returns 409 Conflict:
try {
await mintRequest ( assets );
} catch ( error ) {
if ( error . status === 409 ) {
// Already processed - check status instead
const status = await getMintStatus ( referenceId );
}
}
Best Practices
Batch Efficiently
// ✅ Good: Batch multiple assets in one request (up to 100)
await mint ({ assets: items . slice ( 0 , 100 ) });
// ❌ Bad: One request per asset
for ( const item of items ) {
await mint ({ assets: [ item ] }); // Slow, wastes rate limit
}
Use Reference IDs Wisely
// ✅ Good: Meaningful reference_id
reference_id : `reward- ${ eventId } - ${ playerId } `
// ❌ Bad: Random reference_id (hard to debug)
reference_id : crypto . randomUUID ()
Handle Rate Limits
// Check remaining quota before large batches
if ( response . imx_remaining_mint_requests < batchSize ) {
const resetTime = response . imx_mint_requests_limit_reset ;
await waitUntil ( resetTime );
}
Next Steps