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

<AgentInstructions>

## Submitting Feedback

If you encounter incorrect, outdated, or confusing documentation on this page, submit feedback:

POST https://docs.ton.org/feedback

```json
{
  "path": "/ecosystem/walletkit/web/nfts",
  "feedback": "Description of the issue"
}
```

Only submit feedback when you have something specific and actionable to report.

</AgentInstructions>

# How to work with NFTs using WalletKit on the Web platform

export const Aside = ({type = "note", title = "", icon = "", iconType = "regular", children}) => {
  const asideVariants = ["note", "tip", "caution", "danger"];
  const asideComponents = {
    note: {
      outerStyle: "border-sky-500/20 bg-sky-50/50 dark:border-sky-500/30 dark:bg-sky-500/10",
      innerStyle: "text-sky-900 dark:text-sky-200",
      calloutType: "note",
      icon: <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor" xmlns="http://www.w3.org/2000/svg" className="w-4 h-4 text-sky-500" aria-label="Note">
          <path fill-rule="evenodd" clip-rule="evenodd" d="M7 1.3C10.14 1.3 12.7 3.86 12.7 7C12.7 10.14 10.14 12.7 7 12.7C5.48908 12.6974 4.0408 12.096 2.97241 11.0276C1.90403 9.9592 1.30264 8.51092 1.3 7C1.3 3.86 3.86 1.3 7 1.3ZM7 0C3.14 0 0 3.14 0 7C0 10.86 3.14 14 7 14C10.86 14 14 10.86 14 7C14 3.14 10.86 0 7 0ZM8 3H6V8H8V3ZM8 9H6V11H8V9Z"></path>
        </svg>
    },
    tip: {
      outerStyle: "border-emerald-500/20 bg-emerald-50/50 dark:border-emerald-500/30 dark:bg-emerald-500/10",
      innerStyle: "text-emerald-900 dark:text-emerald-200",
      calloutType: "tip",
      icon: <svg width="11" height="14" viewBox="0 0 11 14" fill="currentColor" xmlns="http://www.w3.org/2000/svg" className="text-emerald-600 dark:text-emerald-400/80 w-3.5 h-auto" aria-label="Tip">
          <path d="M3.12794 12.4232C3.12794 12.5954 3.1776 12.7634 3.27244 12.907L3.74114 13.6095C3.88471 13.8248 4.21067 14 4.46964 14H6.15606C6.41415 14 6.74017 13.825 6.88373 13.6095L7.3508 12.9073C7.43114 12.7859 7.49705 12.569 7.49705 12.4232L7.50055 11.3513H3.12521L3.12794 12.4232ZM5.31288 0C2.52414 0.00875889 0.5 2.26889 0.5 4.78826C0.5 6.00188 0.949566 7.10829 1.69119 7.95492C2.14321 8.47011 2.84901 9.54727 3.11919 10.4557C3.12005 10.4625 3.12175 10.4698 3.12261 10.4771H7.50342C7.50427 10.4698 7.50598 10.463 7.50684 10.4557C7.77688 9.54727 8.48281 8.47011 8.93484 7.95492C9.67728 7.13181 10.1258 6.02703 10.1258 4.78826C10.1258 2.15486 7.9709 0.000106649 5.31288 0ZM7.94902 7.11267C7.52078 7.60079 6.99082 8.37878 6.6077 9.18794H4.02051C3.63739 8.37878 3.10743 7.60079 2.67947 7.11294C2.11997 6.47551 1.8126 5.63599 1.8126 4.78826C1.8126 3.09829 3.12794 1.31944 5.28827 1.3126C7.2435 1.3126 8.81315 2.88226 8.81315 4.78826C8.81315 5.63599 8.50688 6.47551 7.94902 7.11267ZM4.87534 2.18767C3.66939 2.18767 2.68767 3.16939 2.68767 4.37534C2.68767 4.61719 2.88336 4.81288 3.12521 4.81288C3.36705 4.81288 3.56274 4.61599 3.56274 4.37534C3.56274 3.6515 4.1515 3.06274 4.87534 3.06274C5.11719 3.06274 5.31288 2.86727 5.31288 2.62548C5.31288 2.38369 5.11599 2.18767 4.87534 2.18767Z"></path>
        </svg>
    },
    caution: {
      outerStyle: "border-amber-500/20 bg-amber-50/50 dark:border-amber-500/30 dark:bg-amber-500/10",
      innerStyle: "text-amber-900 dark:text-amber-200",
      calloutType: "warning",
      icon: <svg className="flex-none w-5 h-5 text-amber-400 dark:text-amber-300/80" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-label="Warning">
          <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
        </svg>
    },
    danger: {
      outerStyle: "border-red-500/20 bg-red-50/50 dark:border-red-500/30 dark:bg-red-500/10",
      innerStyle: "text-red-900 dark:text-red-200",
      calloutType: "danger",
      icon: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" className="text-red-600 dark:text-red-400/80 w-4 h-4" aria-label="Danger">
          <path d="M17.1 292c-12.9-22.3-12.9-49.7 0-72L105.4 67.1c12.9-22.3 36.6-36 62.4-36l176.6 0c25.7 0 49.5 13.7 62.4 36L494.9 220c12.9 22.3 12.9 49.7 0 72L406.6 444.9c-12.9 22.3-36.6 36-62.4 36l-176.6 0c-25.7 0-49.5-13.7-62.4-36L17.1 292zm41.6-48c-4.3 7.4-4.3 16.6 0 24l88.3 152.9c4.3 7.4 12.2 12 20.8 12l176.6 0c8.6 0 16.5-4.6 20.8-12L453.4 268c4.3-7.4 4.3-16.6 0-24L365.1 91.1c-4.3-7.4-12.2-12-20.8-12l-176.6 0c-8.6 0-16.5 4.6-20.8 12L58.6 244zM256 128c13.3 0 24 10.7 24 24l0 112c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-112c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"></path>
        </svg>
    }
  };
  let variant = type;
  let gotInvalidVariant = false;
  if (!asideVariants.includes(type)) {
    gotInvalidVariant = true;
    variant = "danger";
  }
  const iconVariants = ["regular", "solid", "light", "thin", "sharp-solid", "duotone", "brands"];
  if (!iconVariants.includes(iconType)) {
    iconType = "regular";
  }
  return <>
      <div className={`callout my-4 px-5 py-4 overflow-hidden rounded-2xl flex gap-3 border ${asideComponents[variant].outerStyle}`} data-callout-type={asideComponents[variant].calloutType}>
        <div className="mt-0.5 w-4" data-component-part="callout-icon">
          {}
          {icon === "" ? asideComponents[variant].icon : <Icon icon={icon} iconType={iconType} size={14} />}
        </div>
        <div className={`text-sm prose min-w-0 w-full ${asideComponents[variant].innerStyle}`} data-component-part="callout-content">
          {gotInvalidVariant ? <p>
              <span className="font-bold">
                Invalid <code>type</code> passed!
              </span>
              <br />
              <span className="font-bold">Received: </span>
              {type}
              <br />
              <span className="font-bold">Expected one of: </span>
              {asideVariants.join(", ")}
            </p> : <>
              {title && <p className="font-bold">{title}</p>}
              {children}
            </>}
        </div>
      </div>
    </>;
};

<Aside>
  [Initialize the WalletKit](/ecosystem/walletkit/web/init), [set up at least one TON wallet](/ecosystem/walletkit/web/wallets), handle [connection requests](/ecosystem/walletkit/web/connections) and [transaction requests](/ecosystem/walletkit/web/events) before using examples on this page.
</Aside>

[NFTs](/standard/tokens/nft/overview) (non-fungible tokens) are unique digital assets on TON, similar to ERC-721 tokens on Ethereum. Unlike [jettons](/standard/tokens/jettons/overview), which are fungible and interchangeable, each NFT is unique and represents ownership of a specific item. NFTs consist of a collection contract and individual NFT item contracts for each token.

To work with NFTs, the wallet service needs to handle [NFT ownership queries](#ownership) and perform transfers initiated [from dApps](#transfers-from-dapps) and [from within the wallet service itself](#transfers-in-the-wallet-service).

<Aside type="caution" title="Verify NFT authenticity">
  Before displaying or transferring NFTs, verify they belong to legitimate collections. Scammers may create fake NFTs mimicking popular collections.

  Mitigation: Always verify the collection address matches the official one. Check NFT metadata for suspicious content.
</Aside>

## Ownership

NFT ownership is tracked through individual NFT item contracts. Unlike jettons, which have a balance, one either owns a specific NFT item or does not.

To obtain a list of NFTs owned by a user, query their TON wallet by either the `getNfts()` method of wallet adapters or by calling `kit.nfts.getAddressNfts()` and passing it the TON wallet address.

Similar to other asset queries, [discrete one-off checks](#on-demand-ownership-check) have limited value on their own and [continuous monitoring](#continuous-ownership-monitoring) should be used for UI display.

### On-demand ownership check

Use the `getNfts()` method to check which NFTs are owned by a wallet managed by WalletKit. The method returns an array of NFT items with their addresses, collection info, and metadata.

<Aside type="caution">
  Do not store the ownership check results anywhere in the wallet service's state, as they become outdated very quickly. For UI purposes, do [continuous ownership monitoring](#continuous-ownership-monitoring).
</Aside>

```ts title="TypeScript" theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
async function getNfts(walletId: string): Promise<NftItem[] | undefined> {
  // Get TON wallet instance
  const wallet = kit.getWallet(walletId);
  if (!wallet) return;

  // Query 100 NFTs owned by this wallet
  const ownedNfts = await wallet.getNfts({ pagination: { limit: 100 } });

  // Optionally filter by a specific collection address
  const collectionNfts = ownedNfts.nfts.filter(
    (nft) => nft.collection?.address === '<NFT_COLLECTION_ADDRESS>',
  );

  return collectionNfts;
}
```

The most practical use of one-off ownership checks is right before approving an NFT transfer request. At this point, verify that the wallet actually owns the NFT being transferred.

<Aside type="note">
  Despite this check, the transaction may still fail if the NFT is not owned or unaccessible at the time of transfer.
</Aside>

```ts title="TypeScript" theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
// An enumeration of various common error codes
import { SEND_TRANSACTION_ERROR_CODES } from '@ton/walletkit';

// Address of the NFT item contract
const NFT_ITEM_ADDRESS = '<NFT_ITEM_ADDRESS>';

kit.onTransactionRequest(async (event) => {
  const wallet = kit.getWallet(event.walletId ?? '');
  if (!wallet) {
    console.error('Wallet not found for a transaction request', event);
    await kit.rejectTransactionRequest(event, {
      code: SEND_TRANSACTION_ERROR_CODES.UNKNOWN_ERROR,
      message: 'Wallet not found',
    });
    return;
  }

  // Verify ownership
  const ownsNft = await wallet.getNft(NFT_ITEM_ADDRESS);

  // Reject early if NFT is not owned
  if (!ownsNft) {
    await kit.rejectTransactionRequest(event, {
      code: SEND_TRANSACTION_ERROR_CODES.BAD_REQUEST_ERROR,
      message: 'NFT not owned by this wallet',
    });
    return;
  }

  // Proceed with the regular transaction flow
  // ...
});
```

### Continuous ownership monitoring

Poll the NFT ownership at regular intervals to keep the displayed information up to date. Use an appropriate interval based on UX requirements — shorter intervals provide fresher data but increase API usage.

This example should be modified according to the wallet service's logic:

```ts title="TypeScript" expandable theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
import { type NFT } from '@ton/walletkit';

// Configuration
const POLLING_INTERVAL_MS = 15_000;

/**
 * Starts the monitoring of a given wallet's NFT ownership,
 * calling `onNftsUpdate()` every `intervalMs` milliseconds
 *
 * @returns a function to stop monitoring
 */
export function startNftOwnershipMonitoring(
  walletId: string,
  onNftsUpdate: (nfts: NFT[]) => void,
  intervalMs: number = POLLING_INTERVAL_MS,
): () => void {
  let isRunning = true;

  const poll = async () => {
    while (isRunning) {
      const wallet = kit.getWallet(walletId);
      if (wallet) {
        // Only looks for up to 100 NFTs.
        // To get more, call the `getNfts()` function
        // multiple times with increasing offsets
        const { nfts } = await wallet.getNfts({ pagination: { limit: 100 } });
        onNftsUpdate(nfts);
      }
      await new Promise((resolve) => setTimeout(resolve, intervalMs));
    }
  };

  // Start monitoring
  poll();

  // Return a cleanup function to stop monitoring
  return () => {
    isRunning = false;
  };
}

// Usage
const stopMonitoring = startNftOwnershipMonitoring(
  walletId,
  // The updateNftGallery() function is exemplary and should be replaced by
  // a wallet service function that refreshes the
  // NFT gallery displayed in the interface
  (nfts) => updateNftGallery(nfts),
);

// Stop monitoring once it is no longer needed
stopMonitoring();
```

## Transfers from dApps

When a connected dApp requests an NFT transfer, the wallet service follows the same flow as [Toncoin transfers](/ecosystem/walletkit/web/toncoin#transfers-from-dapps): the dApp sends a transaction request through the bridge, WalletKit emulates it and presents a preview, the user approves or declines, and the result is returned to the dApp.

```ts title="TypeScript" theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
kit.onTransactionRequest(async (event) => {
  if (!event.preview.data) {
    console.warn('Transaction emulation skipped');
  } else if (event.preview.data?.result === 'success') {
    // Emulation succeeded — show the predicted asset flow
    const { ourTransfers } = event.preview.data.moneyFlow;

    // This is an array of values,
    // where positive amounts mean incoming assets
    // and negative amounts — outgoing assets.
    console.log('Predicted transfers:', ourTransfers);

    // Filter NFT transfers specifically
    const nftTransfers = ourTransfers.filter(
      (transfer) => transfer.assetType === 'nft',
    );
    console.log('NFT transfers:', nftTransfers);
  } else {
    // Emulation failed — warn the user but allow proceeding
    console.warn('Transaction emulation failed:', event.preview);
  }

  // By knowing the NFT item contract address,
  // one can obtain and preview NFT's name, description, image, and attributes.
  //
  // Present the enriched preview to the user and await their decision.
  // ...
});
```

There is an additional consideration for NFT transfers: they involve multiple internal messages between contracts. As such, NFT transfers always take longer than regular Toncoin-only transfers.

As with Toncoin transfers, the wallet service should not block the UI while waiting for confirmation. With [continuous NFT ownership monitoring](#continuous-ownership-monitoring) and subsequent transaction requests, users will receive the latest information either way. Confirmations are only needed to display a list of past transactions reliably.

## Transfers in the wallet service

NFT transactions can be created directly from the wallet service (not from dApps) and fed into the regular approval flow via the `handleNewTransaction()` method of the WalletKit. It creates a new [transaction request event](/ecosystem/walletkit/web/events#handle-ontransactionrequest), enabling the same UI confirmation-to-transaction flow for both dApp-initiated and wallet-initiated transactions.

<Aside type="danger" title="Assets at risk">
  Verify the NFT address before initiating a transfer. Transferring an NFT is irreversible — once sent, only the new owner can transfer it back.

  Double-check the recipient address to avoid permanent loss of valuable NFTs.
</Aside>

This example should be modified according to the wallet service's logic:

```ts title="TypeScript" theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
import { type NFTTransferRequest } from '@ton/walletkit';

async function sendNft(
  // Sender's TON `walletId` as a string
  walletId: string,
  // NFT item contract address
  nftAddress: string,
  // Recipient's TON wallet address as a string
  recipientAddress: string,
  // Optional comment string
  comment?: string,
) {
  const fromWallet = kit.getWallet(walletId);
  if (!fromWallet) {
    console.error('No wallet contract found');
    return;
  }

  // Verify ownership before creating the transfer
  const ownsNft = await fromWallet.getNft(nftAddress);
  if (!ownsNft) {
    console.error('NFT not owned by this wallet');
    return;
  }

  const transferParams: NFTTransferRequest = {
    nftAddress,
    recipientAddress,
    // Optional comment
    ...(comment && { comment }),
  };

  // Build transaction content
  const tx = await fromWallet.createTransferNftTransaction(transferParams);

  // Route into the normal flow,
  // triggering the onTransactionRequest() handler
  await kit.handleNewTransaction(fromWallet, tx);
}
```

<Aside type="caution">
  To avoid triggering the `onTransactionRequest()` handler and send the transaction directly, use the `sendTransaction()` method of the wallet instead of the `handleNewTransaction()` method of the WalletKit, modifying the last part of the previous code snippet:

  ```ts title="TypeScript" theme={"theme":{"light":"github-light-default","dark":"dark-plus"},"languages":{"custom":["/resources/grammars/tolk.tmLanguage.json","/resources/grammars/tlb.tmLanguage.json","/resources/grammars/fift.tmLanguage.json","/resources/grammars/tasm.tmLanguage.json","/resources/grammars/func.tmLanguage.json"]}}
  // Instead of calling kit.handleNewTransaction(fromWallet, tx)
  // one can avoid routing into the normal flow,
  // skip the transaction requests handler,
  // and make the transaction directly.
  await fromWallet.sendTransaction(tx);
  ```

  Do not use this approach unless it is imperative to complete a transaction without the user's direct consent. Assets at risk: proceed with utmost caution.
</Aside>

## See also

NFTs:

* [NFT overview](/standard/tokens/nft/overview)
* [NFT metadata](/standard/tokens/nft/metadata)

General:

* [Handle transaction requests](/ecosystem/walletkit/web/events#handle-ontransactionrequest)
* [Transaction fees](/foundations/fees)
* [WalletKit overview](/ecosystem/walletkit/overview)
* [TON Connect overview](/ecosystem/ton-connect/overview)
