Sell NFTs with payment collection, order verification, and minting—all in a single widget.
When to use:
NFT drops and launches
In-game item purchases
Digital collectible storefronts
How it works
User selects items → Widget handles payment → Your backend verifies → NFT minted to wallet
The widget handles payment collection (crypto + fiat), wallet connection, and insufficient balance flows. Your backend handles order creation, payment verification, and NFT minting.
Prerequisites
Setup
Configure in Hub
Go to Hub → Primary Sales to deploy a Primary Sales contract, configure payment currencies and payout addresses, and choose between Simplified or Advanced mode.
Integrate the widget
Mount the Sale widget in your frontend to handle the checkout flow.
Handle backend (Advanced mode only)
Implement webhook endpoints if using Advanced mode for dynamic pricing or custom logic.
import { checkout } from '@imtbl/sdk' ;
import { Environment } from '@imtbl/sdk/config' ;
const { Checkout , WidgetType , WidgetTheme , CommerceFlowType , CommerceEventType } = checkout ;
const checkoutSDK = new Checkout ({
baseConfig: {
environment: Environment . SANDBOX ,
publishableKey: 'YOUR_PUBLISHABLE_KEY' ,
},
});
export async function openSale (
elementId : string ,
items : checkout . SaleItem [],
environmentId : string
) {
const widgets = await checkoutSDK . widgets ({ config: { theme: WidgetTheme . DARK } });
const widget = widgets . create ( WidgetType . IMMUTABLE_COMMERCE );
widget . mount ( elementId , {
flow: CommerceFlowType . SALE ,
items ,
environmentId ,
});
widget . addListener ( CommerceEventType . SUCCESS , ( data ) => {
console . log ( 'Purchase complete:' , data );
widget . unmount ();
});
widget . addListener ( CommerceEventType . FAILURE , ( data ) => {
console . error ( 'Purchase failed:' , data );
widget . unmount ();
});
widget . addListener ( CommerceEventType . CLOSE , () => {
widget . unmount ();
});
return widget ;
}
// Example items
const items : checkout . SaleItem [] = [
{
productId: 'starter-pack' ,
name: 'Starter Pack' ,
description: '10 cards + 500 gold' ,
image: 'https://example.com/pack.png' ,
qty: 1 ,
},
];
Parameters
Parameter Type Description flow'SALE'Required. Specifies the sale flow. itemsSaleItem[]Items to purchase. environmentIdstringYour Hub environment ID. collectionNamestringCollection name (display only). excludePaymentTypesstring[]Payment methods to hide.
Events
Event Description SUCCESSPurchase completed. Payload: { transactionHash, tokens } FAILUREPurchase failed. Payload: { error } CLOSEUser closed widget.
Sale modes
Create and manage products directly in Hub—no backend required.
Products created in Hub are automatically available for sale
Stock management handled by the widget
Token IDs auto-generated for ERC-721, specified for ERC-1155
Products API: Your storefront can fetch products from the public API: # Testnet
https://api.sandbox.immutable.com/v1/primary-sales/:environmentId/products
# Mainnet
https://api.immutable.com/v1/primary-sales/:environmentId/products
Metadata for minted tokens:
ERC-1155 : Pre-define metadata for the Token ID before sales begin
ERC-721 : Set up a webhook in Hub for imtbl_zkevm_activity_mint events, then generate metadata and call the Metadata Refresh API
Implement webhook endpoints for full control over pricing, stock, and order fulfillment. When to use:
Dynamic pricing based on user or market conditions
Complex inventory management
Custom eligibility rules
Integration with existing e-commerce systems
Configure your webhook base URL in Hub. The widget calls your backend at four lifecycle stages: User selects items → Quote → User reviews → Authorize → Payment executes → Confirm
↓
(if payment fails) Expired
Webhooks (Advanced mode)
Called when a user requests a price quote for items. Endpoint: POST {baseUrl}/quote Request/Response
Implementation
Request: {
"recipient_address" : "0x..." ,
"products" : [
{ "product_id" : "starter-pack" , "quantity" : 1 }
],
"currency" : "USDC"
}
Response: {
"products" : [
{
"product_id" : "starter-pack" ,
"quantity" : 1 ,
"pricing" : [
{
"currency" : "USDC" ,
"currency_type" : "ERC20" ,
"amount" : "10000000" ,
"contract_address" : "0x..."
}
]
}
],
"totals" : {
"subtotal" : "10000000" ,
"fees" : "0" ,
"total" : "10000000"
},
"currency" : "USDC" ,
"expires_at" : "2024-01-01T12:00:00Z"
}
app . post ( '/quote' , async ( req , res ) => {
const { recipient_address , products , currency } = req . body ;
// Validate products exist and are in stock
const validatedProducts = await validateProducts ( products );
// Calculate pricing (can be dynamic based on user, time, etc.)
const pricing = await calculatePricing ( validatedProducts , currency );
// Set expiry (e.g., 5 minutes)
const expires_at = new Date ( Date . now () + 5 * 60 * 1000 ). toISOString ();
res . json ({
products: pricing . products ,
totals: pricing . totals ,
currency ,
expires_at ,
});
});
Called when the user confirms the order and is ready to pay. Reserve inventory here. Endpoint: POST {baseUrl}/authorize Request/Response
Implementation
Request: {
"reference" : "order-123" ,
"recipient_address" : "0x..." ,
"products" : [
{
"product_id" : "starter-pack" ,
"quantity" : 1 ,
"collection_address" : "0x..." ,
"token_ids" : [ "12345" ]
}
],
"currency" : "USDC" ,
"total_amount" : "10000000"
}
Response: {
"reference" : "order-123" ,
"authorized" : true
}
app . post ( '/authorize' , async ( req , res ) => {
const { reference , recipient_address , products , total_amount } = req . body ;
try {
// Reserve inventory
await reserveInventory ( reference , products );
// Store order for later confirmation
await createPendingOrder ({
reference ,
recipient_address ,
products ,
total_amount ,
status: 'pending' ,
});
res . json ({ reference , authorized: true });
} catch ( error ) {
res . json ({ reference , authorized: false , reason: error . message });
}
});
Called after successful on-chain payment. Mint the NFTs here. Endpoint: POST {baseUrl}/confirm Request/Response
Implementation
Request: {
"reference" : "order-123" ,
"recipient_address" : "0x..." ,
"tx_hash" : "0x..." ,
"products" : [
{
"product_id" : "starter-pack" ,
"quantity" : 1 ,
"collection_address" : "0x..." ,
"token_ids" : [ "12345" ]
}
]
}
Response: {
"reference" : "order-123" ,
"confirmed" : true
}
app . post ( '/confirm' , async ( req , res ) => {
const { reference , recipient_address , tx_hash , products } = req . body ;
try {
// Verify the transaction on-chain
const tx = await provider . getTransaction ( tx_hash );
if ( ! tx || tx . to !== PRIMARY_SALES_CONTRACT ) {
throw new Error ( 'Invalid transaction' );
}
await tx . wait ( 1 );
// Mint NFTs via Minting API
for ( const product of products ) {
for ( const tokenId of product . token_ids ) {
await mintNFT ({
recipient: recipient_address ,
contractAddress: product . collection_address ,
tokenId ,
metadata: await getMetadata ( product . product_id ),
});
}
}
// Update order status
await updateOrderStatus ( reference , 'completed' );
res . json ({ reference , confirmed: true });
} catch ( error ) {
res . json ({ reference , confirmed: false , reason: error . message });
}
});
Called when an authorized order expires without payment. Release reserved inventory. Endpoint: POST {baseUrl}/expired Request/Response
Implementation
Request: {
"reference" : "order-123" ,
"recipient_address" : "0x..." ,
"products" : [
{ "product_id" : "starter-pack" , "quantity" : 1 }
]
}
Response: {
"reference" : "order-123" ,
"expired" : true
}
app . post ( '/expired' , async ( req , res ) => {
const { reference , products } = req . body ;
// Release reserved inventory
await releaseInventory ( reference , products );
// Update order status
await updateOrderStatus ( reference , 'expired' );
res . json ({ reference , expired: true });
});
Webhook security
Validate incoming webhooks to ensure they’re from Immutable:
import crypto from 'crypto' ;
function verifyWebhookSignature (
payload : string ,
signature : string ,
secret : string
) : boolean {
const expectedSignature = crypto
. createHmac ( 'sha256' , secret )
. update ( payload )
. digest ( 'hex' );
return crypto . timingSafeEqual (
Buffer . from ( signature ),
Buffer . from ( expectedSignature )
);
}
// Middleware
app . use ( '/webhooks' , ( req , res , next ) => {
const signature = req . headers [ 'x-imtbl-signature' ];
const isValid = verifyWebhookSignature (
JSON . stringify ( req . body ),
signature ,
process . env . WEBHOOK_SECRET
);
if ( ! isValid ) {
return res . status ( 401 ). json ({ error: 'Invalid signature' });
}
next ();
});
Payment options
Crypto
Users can pay with tokens on Immutable Chain. Default currencies are USDC and IMX. Custom ERC-20 tokens must be whitelisted.
If users don’t have enough tokens, the widget offers swap , bridge , or fiat onramp options.
Fiat
Users can pay with credit card, Apple Pay, or Google Pay. Funds settle to your wallet in USDC. KYC may be required for larger amounts.
Limits
Maximum 350 items per transaction due to gas limits. For larger orders, split into multiple transactions.
Next steps