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 } = checkout;

const checkoutSDK = new Checkout({
  baseConfig: {
    environment: Environment.SANDBOX, // or Environment.PRODUCTION
    publishableKey: 'YOUR_PUBLISHABLE_KEY',
  },
});
const { CommerceFlowType, CommerceEventType } = checkout;

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 APIList products (read)Storefronts and backends can fetch the catalog without authentication:
# Testnet
GET https://api.sandbox.immutable.com/v1/primary-sales/:environmentId/products

# Mainnet
GET https://api.immutable.com/v1/primary-sales/:environmentId/products
Create or update a productUpserts a single product by ID (pricing, stock, metadata, linked collection and token rules, etc.). Same URL for create and update.
# Testnet
PUT https://api.sandbox.immutable.com/v1/primary-sales/:environmentId/product

# Mainnet
PUT https://api.immutable.com/v1/primary-sales/:environmentId/product
PUT replaces the entire product record for that product_id. Send a JSON object with the shape below. A successful response returns the persisted product (same fields as GET /products).Product
FieldTypeRequiredDescription
product_idstringYesStable catalog identifier (for example starter-pack).
namestringYesDisplay name.
imagestringYesURL for product art or icon.
descriptionstringNoLonger description shown in UIs that support it.
statusstringYesactive or inactive. Inactive products cannot be purchased in Simplified mode.
quantityintegerYesStock available for sale (inventory).
pricingarrayYesOne or more price rows (see Pricing below). At least one entry is required.
collectionobjectYesWhich collection and token rules apply at mint (see Collection below).
limitsobjectNoOptional per-buyer caps (see Limits below).
Pricing (pricing[])Each element defines a price in one currency:
FieldTypeRequiredDescription
currencystringYesCurrency code (for example USDC), aligned with currencies configured for Primary Sales.
amountnumberYesUnit price in that currency for a single item.
Collection (collection)
FieldTypeRequiredDescription
collection_typestringYesERC721 or ERC1155. Must match how the linked collection was deployed.
collection_addressstringYesNFT collection contract on chain (same collection linked to Primary Sales in Hub).
token_idstringERC-1155 onlyFixed token ID for this product. Required for ERC-1155. Omit or leave unset for ERC-721 (mints use generated IDs).
Limits (limits)
FieldTypeRequiredDescription
enabledbooleanYes if limits is sentWhen true, per-recipient limits apply.
max_per_recipient_addressintegerIf enabledMaximum units one wallet may purchase; must be greater than zero when limits are enabled.
Example (ERC-721)
{
  "product_id": "starter-pack",
  "name": "Starter Pack",
  "description": "10 cards + 500 gold",
  "image": "https://example.com/pack.png",
  "status": "active",
  "quantity": 500,
  "pricing": [{ "currency": "USDC", "amount": 9.99 }],
  "collection": {
    "collection_type": "ERC721",
    "collection_address": "0x0000000000000000000000000000000000000000"
  },
  "limits": { "enabled": false }
}
Use your real collection address from Hub in place of the placeholder collection_address.Delete a productRemoves a product from the catalog for that environment.
# Testnet
DELETE https://api.sandbox.immutable.com/v1/primary-sales/:environmentId/product/:productId

# Mainnet
DELETE https://api.immutable.com/v1/primary-sales/:environmentId/product/:productId
AuthenticationListing products is public; create, update, and delete require authentication. Authentication requires either:
MethodHeaderWhen to use
Bearer tokenAuthorization: Bearer <access_token>Used when managing the product catalogue via Hub
Secret API keyx-immutable-api-key: <secret_key>Server-side scripts, automation, or backends using a secret API key issued for your project.
Navigate to the Settings page of the project environment to create a secret API key. Do not expose secret keys in client-side code. Only manage products from your backend or Hub.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)
  );
}

// In your webhook handler
app.post('/webhooks/immutable', (req, res) => {
  const signature = req.headers['x-immutable-signature'];
  const isValid = verifyWebhookSignature(
    JSON.stringify(req.body),
    signature,
    process.env.WEBHOOK_SECRET
  );
  
  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }
  
  // Process the webhook
  handleWebhookEvent(req.body);
  res.status(200).send('OK');
});

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

Minting API

Mint NFTs from your backend

Hub Webhooks

Configure webhooks for mint events

ERC-721 Contracts

Deploy NFT collections

Orderbook

Enable secondary trading