> ## Documentation Index
> Fetch the complete documentation index at: https://docs.immutable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Minting API

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`                          |

<Info>
  The Minting API is a REST API. See the endpoint examples below for request/response formats.
</Info>

## Prerequisites

<CardGroup cols={3}>
  <Card title="Deploy Contract" icon="file-contract" href="/docs/products/asset-contracts/erc721">
    ERC-721 or ERC-1155 contract
  </Card>

  <Card title="Enable Minting API" icon="toggle-on" href="/docs/products/hub/deploy-contracts">
    Grant minter role to Immutable
  </Card>

  <Card title="API Keys" icon="key" href="/docs/products/hub/api-keys">
    Secret API Key for authentication
  </Card>
</CardGroup>

<Warning>
  The Minting API requires [Immutable preset contracts](/docs/products/asset-contracts/overview). Custom contracts are not supported.
</Warning>

<Warning>
  **Security:** Keep your Secret API Key server-side only. See [API Keys Security](/docs/products/hub/api-keys#secret-key) for best practices.
</Warning>

## 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 |

<Info>
  Rate limits count distinct `token_id`s, not total tokens. Minting 1000 copies of the same ERC-1155 `token_id` counts as 1 request.
</Info>

Rate limit headers in response:

```json theme={null}
{
  "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_id`s (e.g., migrating existing assets):

```typescript theme={null}
// 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_id`s:

```typescript theme={null}
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.

<Warning>
  `mintBatchByQuantity` may return incomplete `balanceOf()` results for collections with 80,000+ tokens. Contact Immutable if you need both features.
</Warning>

## Minting ERC-1155

```typescript theme={null}
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_id`           | Always required                         |
| `amount`             | Required (number of tokens)             |
| Metadata             | Only on **first** mint for a `token_id` |
| Duplicate `token_id` | Allowed (mints additional quantity)     |

<Warning>
  Passing metadata when minting to an existing `token_id` returns `409 Conflict`. Check if the token exists before including metadata.
</Warning>

## Tracking Mint Status

```typescript theme={null}
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                                                     |
| ----------- | ----------------------------------------------------------- |
| `pending`   | Request queued, waiting for blockchain confirmation         |
| `succeeded` | Mint completed, `token_id` and `transaction_hash` available |
| `failed`    | Mint failed, see `error` field                              |

### Webhooks (Recommended)

Configure webhooks in [Hub](https://hub.immutable.com) to receive real-time updates:

```typescript theme={null}
// 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...',
  }
}
```

## Metadata

### Include with Mint Request (Recommended)

Include metadata in the mint request for immediate indexing:

```typescript theme={null}
{
  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.

<Card title="Metadata Format" icon="file-code" href="/docs/products/asset-contracts/erc721#metadata">
  See the metadata format specification
</Card>

## Refreshing Metadata

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.

<Warning>
  Immutable does not listen for URI update events. You must explicitly trigger a metadata refresh for changes to propagate across the ecosystem.
</Warning>

### When to Refresh Metadata

| 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:

```typescript theme={null}
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
);
```

<Warning>
  Submit the **entire** asset metadata when refreshing—not just changed attributes. Omitted fields will be removed.
</Warning>

### 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:

```json theme={null}
{
  "imx_refreshes_limit": "10000",
  "imx_refresh_limit_reset": "2024-02-13 15:04:05",
  "imx_remaining_refreshes": "9000",
  "retry_after": "2-seconds"
}
```

<Tip>
  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.
</Tip>

### Verify Metadata Updated

Metadata refreshes are asynchronous. Check the `metadata_synced_at` field via the [Indexer](/docs/products/indexer/overview) to confirm updates:

```typescript theme={null}
// 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_ERROR`        | 400  | >100 assets, duplicate `reference_id`, invalid format |
| `UNAUTHORISED_REQUEST`    | 401  | Invalid API key                                       |
| `AUTHENTICATION_ERROR`    | 403  | Collection not linked to API key                      |
| `CONFLICT`                | 409  | `reference_id` or `token_id` already used             |
| `TOO_MANY_REQUESTS_ERROR` | 429  | Rate limit exceeded                                   |

### Transaction Errors (Check via Status API)

| Error                                        | Cause                    | Solution                                        |
| -------------------------------------------- | ------------------------ | ----------------------------------------------- |
| `AccessControl: account ... is missing role` | Minter role not granted  | Grant minter role to Immutable's minting wallet |
| `Exceeded maximum submissions`               | Transaction retry failed | Resubmit the request                            |

### Idempotent Requests

Safe to retry failed network requests—duplicate `reference_id` returns `409 Conflict`:

```typescript theme={null}
try {
  await mintRequest(assets);
} catch (error) {
  if (error.status === 409) {
    // Already processed - check status instead
    const status = await getMintStatus(referenceId);
  }
}
```

## Best Practices

### Batch Efficiently

```typescript theme={null}
// ✅ 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

```typescript theme={null}
// ✅ Good: Meaningful reference_id
reference_id: `reward-${eventId}-${playerId}`

// ❌ Bad: Random reference_id (hard to debug)
reference_id: crypto.randomUUID()
```

### Handle Rate Limits

```typescript theme={null}
// 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

<CardGroup cols={2}>
  <Card title="Build a Game" icon="gamepad" href="/docs/guides/build-a-game">
    Backend minting in games
  </Card>

  <Card title="ERC-721" icon="file-contract" href="/docs/products/asset-contracts/erc721">
    NFT contracts
  </Card>

  <Card title="ERC-1155" icon="file-contract" href="/docs/products/asset-contracts/erc1155">
    Multi-token contracts
  </Card>

  <Card title="Indexer" icon="database" href="/docs/products/indexer/overview">
    Query minted assets
  </Card>

  <Card title="Webhooks" icon="webhook" href="/docs/products/indexer#webhook-integration">
    Real-time mint notifications
  </Card>
</CardGroup>
