Skip to main content
Sell NFTs with payment collection, order verification, and minting—all in a single widget. Primary Sales 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

1

Configure in Hub

Go to HubPrimary Sales to deploy a Primary Sales contract, configure payment currencies and payout addresses, and choose between Simplified or Advanced mode.
2

Integrate the widget

Mount the Sale widget in your frontend to handle the checkout flow.
3

Handle backend (Advanced mode only)

Implement webhook endpoints if using Advanced mode for dynamic pricing or custom logic.

Widget integration

npm install @imtbl/sdk
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

ParameterTypeDescription
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

EventDescription
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

Webhooks (Advanced mode)

Called when a user requests a price quote for items.Endpoint: POST {baseUrl}/quote
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"
}
Called when the user confirms the order and is ready to pay. Reserve inventory here.Endpoint: POST {baseUrl}/authorize
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
}
Called after successful on-chain payment. Mint the NFTs here.Endpoint: POST {baseUrl}/confirm
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
}
Called when an authorized order expires without payment. Release reserved inventory.Endpoint: POST {baseUrl}/expired
Request:
{
  "reference": "order-123",
  "recipient_address": "0x...",
  "products": [
    { "product_id": "starter-pack", "quantity": 1 }
  ]
}
Response:
{
  "reference": "order-123",
  "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