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

# Primary Sales

Sell NFTs with payment collection, order verification, and minting—all in a single widget.

<img src="https://mintcdn.com/immutable-b818fae7/a-_df05Z16XVB-P7/images/checkout/primary-sales.webp?fit=max&auto=format&n=a-_df05Z16XVB-P7&q=85&s=b3276b5cb6294b14853b7c278bf7cb45" alt="Primary Sales Widget" noZoom style={{float: 'right', width: '200px', borderRadius: '8px', marginLeft: '24px', marginBottom: '16px'}} width="860" height="1300" data-path="images/checkout/primary-sales.webp" />

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

* [Hub project](/docs/products/hub/getting-started) with an environment
* [ERC-721](/docs/products/asset-contracts/erc721) or [ERC-1155](/docs/products/asset-contracts/erc1155) collection deployed

## Setup

<Steps>
  <Step title="Configure in Hub">
    Go to [Hub](https://hub.immutable.com) → **Primary Sales** to deploy a Primary Sales contract, configure payment currencies and payout addresses, and choose between Simplified or Advanced mode.
  </Step>

  <Step title="Integrate the widget">
    Mount the Sale widget in your frontend to handle the checkout flow.
  </Step>

  <Step title="Handle backend (Advanced mode only)">
    Implement webhook endpoints if using Advanced mode for dynamic pricing or custom logic.
  </Step>
</Steps>

## Widget integration

<Tabs>
  <Tab title="npm">
    ```bash theme={null}
    npm install @imtbl/sdk
    ```
  </Tab>

  <Tab title="yarn">
    ```bash theme={null}
    yarn add @imtbl/sdk
    ```
  </Tab>
</Tabs>

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

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

| Parameter             | Type         | Description                        |
| --------------------- | ------------ | ---------------------------------- |
| `flow`                | `'SALE'`     | Required. Specifies the sale flow. |
| `items`               | `SaleItem[]` | Items to purchase.                 |
| `environmentId`       | `string`     | Your Hub environment ID.           |
| `collectionName`      | `string`     | Collection name (display only).    |
| `excludePaymentTypes` | `string[]`   | Payment methods to hide.           |

### Events

| Event     | Description                                                |
| --------- | ---------------------------------------------------------- |
| `SUCCESS` | Purchase completed. Payload: `{ transactionHash, tokens }` |
| `FAILURE` | Purchase failed. Payload: `{ error }`                      |
| `CLOSE`   | User closed widget.                                        |

## Sale modes

<Tabs>
  <Tab title="Simplified">
    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**

    **List 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 product**

    Upserts 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**

    | Field         | Type    | Required | Description                                                                           |
    | ------------- | ------- | -------- | ------------------------------------------------------------------------------------- |
    | `product_id`  | string  | Yes      | Stable catalog identifier (for example `starter-pack`).                               |
    | `name`        | string  | Yes      | Display name.                                                                         |
    | `image`       | string  | Yes      | URL for product art or icon.                                                          |
    | `description` | string  | No       | Longer description shown in UIs that support it.                                      |
    | `status`      | string  | Yes      | `active` or `inactive`. Inactive products cannot be purchased in **Simplified** mode. |
    | `quantity`    | integer | Yes      | Stock available for sale (inventory).                                                 |
    | `pricing`     | array   | Yes      | One or more price rows (see **Pricing** below). At least one entry is required.       |
    | `collection`  | object  | Yes      | Which collection and token rules apply at mint (see **Collection** below).            |
    | `limits`      | object  | No       | Optional per-buyer caps (see **Limits** below).                                       |

    **Pricing** (`pricing[]`)

    Each element defines a price in one currency:

    | Field      | Type   | Required | Description                                                                               |
    | ---------- | ------ | -------- | ----------------------------------------------------------------------------------------- |
    | `currency` | string | Yes      | Currency code (for example `USDC`), aligned with currencies configured for Primary Sales. |
    | `amount`   | number | Yes      | Unit price in that currency for a single item.                                            |

    **Collection** (`collection`)

    | Field                | Type   | Required      | Description                                                                                                            |
    | -------------------- | ------ | ------------- | ---------------------------------------------------------------------------------------------------------------------- |
    | `collection_type`    | string | Yes           | `ERC721` or `ERC1155`. Must match how the linked collection was deployed.                                              |
    | `collection_address` | string | Yes           | NFT collection contract on chain (same collection linked to Primary Sales in Hub).                                     |
    | `token_id`           | string | ERC-1155 only | Fixed token ID for this product. **Required** for ERC-1155. Omit or leave unset for ERC-721 (mints use generated IDs). |

    **Limits** (`limits`)

    | Field                       | Type    | Required                | Description                                                                               |
    | --------------------------- | ------- | ----------------------- | ----------------------------------------------------------------------------------------- |
    | `enabled`                   | boolean | Yes if `limits` is sent | When `true`, per-recipient limits apply.                                                  |
    | `max_per_recipient_address` | integer | If enabled              | Maximum units one wallet may purchase; must be greater than zero when limits are enabled. |

    **Example (ERC-721)**

    ```json theme={null}
    {
      "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 product**

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

    **Authentication**

    Listing products is public; **create, update, and delete require authentication**. Authentication requires either:

    | Method             | Header                                 | When to use                                                                                                                 |
    | ------------------ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
    | **Bearer token**   | `Authorization: Bearer <access_token>` | Used when managing the product catalogue via Hub                                                                            |
    | **Secret API key** | `x-immutable-api-key: <secret_key>`    | Server-side scripts, automation, or backends using a [secret API key](/docs/products/hub/api-keys) 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](/docs/products/hub/webhooks) for `imtbl_zkevm_activity_mint` events, then generate metadata and call the [Metadata Refresh API](/docs/products/indexer/overview)
  </Tab>

  <Tab title="Advanced">
    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
    ```
  </Tab>
</Tabs>

## Webhooks (Advanced mode)

<AccordionGroup>
  <Accordion title="Quote">
    Called when a user requests a price quote for items.

    **Endpoint:** `POST {baseUrl}/quote`

    <Tabs>
      <Tab title="Request/Response">
        **Request:**

        ```json theme={null}
        {
          "recipient_address": "0x...",
          "products": [
            { "product_id": "starter-pack", "quantity": 1 }
          ],
          "currency": "USDC"
        }
        ```

        **Response:**

        ```json theme={null}
        {
          "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"
        }
        ```
      </Tab>

      <Tab title="Implementation">
        ```typescript theme={null}
        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,
          });
        });
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="Authorize">
    Called when the user confirms the order and is ready to pay. Reserve inventory here.

    **Endpoint:** `POST {baseUrl}/authorize`

    <Tabs>
      <Tab title="Request/Response">
        **Request:**

        ```json theme={null}
        {
          "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:**

        ```json theme={null}
        {
          "reference": "order-123",
          "authorized": true
        }
        ```
      </Tab>

      <Tab title="Implementation">
        ```typescript theme={null}
        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 });
          }
        });
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="Confirm">
    Called after successful on-chain payment. Mint the NFTs here.

    **Endpoint:** `POST {baseUrl}/confirm`

    <Tabs>
      <Tab title="Request/Response">
        **Request:**

        ```json theme={null}
        {
          "reference": "order-123",
          "recipient_address": "0x...",
          "tx_hash": "0x...",
          "products": [
            {
              "product_id": "starter-pack",
              "quantity": 1,
              "collection_address": "0x...",
              "token_ids": ["12345"]
            }
          ]
        }
        ```

        **Response:**

        ```json theme={null}
        {
          "reference": "order-123",
          "confirmed": true
        }
        ```
      </Tab>

      <Tab title="Implementation">
        ```typescript theme={null}
        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 });
          }
        });
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="Expired">
    Called when an authorized order expires without payment. Release reserved inventory.

    **Endpoint:** `POST {baseUrl}/expired`

    <Tabs>
      <Tab title="Request/Response">
        **Request:**

        ```json theme={null}
        {
          "reference": "order-123",
          "recipient_address": "0x...",
          "products": [
            { "product_id": "starter-pack", "quantity": 1 }
          ]
        }
        ```

        **Response:**

        ```json theme={null}
        {
          "reference": "order-123",
          "expired": true
        }
        ```
      </Tab>

      <Tab title="Implementation">
        ```typescript theme={null}
        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 });
        });
        ```
      </Tab>
    </Tabs>
  </Accordion>
</AccordionGroup>

### Webhook security

Validate incoming webhooks to ensure they're from Immutable:

```typescript theme={null}
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](/docs/products/checkout/swap), [bridge](/docs/products/checkout/bridge), or [fiat onramp](/docs/products/checkout/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

<Warning>
  Maximum **350 items** per transaction due to gas limits. For larger orders, split into multiple transactions.
</Warning>

## Next steps

<CardGroup cols={2}>
  <Card title="Minting API" icon="wand-magic-sparkles" href="/docs/products/asset-contracts/minting-api">
    Mint NFTs from your backend
  </Card>

  <Card title="Hub Webhooks" icon="webhook" href="/docs/products/hub/webhooks">
    Configure webhooks for mint events
  </Card>

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

  <Card title="Orderbook" icon="arrow-right-arrow-left" href="/docs/products/orderbook/overview">
    Enable secondary trading
  </Card>
</CardGroup>
