Get Started with the Hedera Token Service - Part 1: How to… | Hedera Hedera Network Services Token Service Mint and configure tokens and accounts. Consensus Service Verifiable timestamps and ordering of events. Smart Contracts Run Solidity smart contracts. HBAR The Hedera network's native cryptocurrency. Insights How It Works Learn about Hedera from end to end. Explorers View live and historical data on Hedera. Dashboards Analyze network activity and metrics. Network Nodes Understand networks and node types. Devs Start Building Get Started Learn core concepts and build the future. Documentation Review the API and build using your favorite language. Developer Resources Integrations Plugins and microservices for Hedera. Fee Estimator Understand and estimate transaction costs. Open Source Hedera is committed to open, transparent code. Learning Center Learn about web3 and blockchain technologies. Grants Grants & accelerators for your project. Bounties Find bugs. Submit a report. Earn rewards. Ecosystem ECOSYSTEM Hedera Ecosystem Applications, developer tools, network explorers, and more. NFT Ecosystem Metrics Analyze on-chain and market NFT ecosystem metrics. CATEGORIES Web3 Applications Connect into the innovative startups decentralizing the web on Hedera. Enterprise Applications Learn about the Fortune 500 companies decentralizing the web on Hedera. Wallets & Custodians Create a Hedera account to manage HBAR, fungible tokens, and NFTs. Network Explorers Hedera mainnet and testnet graphical network explorers. Developer Tooling Third-party APIs, integrations, and plugins to build apps on Hedera. Grants & Accelerators Boost your project with support from the Hedera ecosystem. Partner Program Explore our partners to bring your vision into reality. Hedera Council Over 30 highly diversified organizations govern Hedera. Use Cases Hedera Solutions Asset Tokenization Studio Open source toolkit for tokenizing assets securely. Stablecoin Studio All-in-one toolkit for stablecoin solutions. Hedera Guardian Auditable carbon markets and traceability. Functional Use Cases Data Integrity & AI Reliable, secure, and ethically governed insights. Sustainability Enabling fair carbon markets with trust. Real-World Asset Tokenization Seamless tokenization of real-world assets and digital at scale. Consumer Engagement & Loyalty Mint, distribute, and redeem loyalty rewards. Decentralized Identity Maintain the lifecycle of credentials. Decentralized Logs Scalable, real-time timestamped events. DeFi Dapps built for the next-generation of finance. NFTs Low, fixed fees. Immutable royalties. Payments Scalable, real-time, and affordable crypto-payments. HBAR Overview Learn about Hedera's token, HBAR. Treasury Management Hedera’s report of the HBAR supply. Governance Decentralized Governance Hedera Council See the world's leading organizations that own Hedera. About Meet Hedera's Board of Directors and team. Journey Watch Hedera's journey to build an empowered digital future for all. Transparent Governance Public Policy Hedera's mission is to inform policy and regulation that impact the industry. Meeting Minutes Immutably recorded on Hedera. Roadmap Follow Hedera's roadmap in its journey to build the future. Resources Company What's New Partners Papers Careers Media Blog Technical Press Podcast Community Events Meetups Store Brand Navigation QUICKSTART Get Started with the Hedera Token Service - Part 1: How to Mint NFTs technical Oct 26, 2021 by Ed Marquez Head of Developer Relations Hedera Token Service (HTS) enables you to configure, mint, and manage tokens on the Hedera network without the need to set up and deploy a smart contract. Tokens are as fast, fair, and secure as hbar and cost a fraction of 1¢ USD to transfer. Let’s look at some of the functionality available to you with HTS. You will see that the ease-of-use and flexibility this service provides make HTS tokens an excellent alternative to tokens with smart contracts on Ethereum. For those coming from Ethereum, HTS functionality can be mapped to multiple types of ERC token standards including ERC20, ERC721, and ERC1155 – you can learn more about the mapping in this blog post. And starting in early 2022, you will be able to use HTS with smart contracts for those use cases where you need advanced logic and programmability for your tokens. In this part of the series, you will learn how to: Create a custom fee schedule Configure a non-fungible token (NFT) Mint and burn NFTs Associate and Transfer NFTs We will configure an NFT art collection for autumn images. Note that with HTS you can also create fungible tokens that can represent anything from a stablecoin pegged to the USD value, or an in-game reward system.  Note: To make sure you have everything you need to follow along, be sure to check out these getting started resources. There, you will see how to create a Hedera testnet account, and then you can configure your development environment. If you want the entire code used, skip to the Code Check section below.  Remember that while the following examples are in JavaScript, official SDKs supporting Go and Java are also available and implemented very similarly, alongside community-supported SDKs in .NET and various other frameworks or languages. Create New Hedera Accounts and Generate Keys for the NFT Let's create additional Hedera accounts to represent users for this scenario like the Treasury, Alice, and Bob. These accounts are created using your Testnet account credentials from the Hedera portal (see the getting started resources). Account creation starts by generating a private key for the new account and then calling a reusable function (accountCreatorFcn) that uses the new key, an initial balance, and the Hedera client. You can easily reuse this function if you need to create more accounts in the future. Once accounts are created for Treasury, Alice, and Bob, new private keys are generated to manage specific token functionality. When specifying the key value for specific functionality, be sure to always provide the corresponding public key. Never expose or share your private key(s) with others, as that may result in lost funds, or loss of control over your account. // CREATE NEW HEDERA ACCOUNTS TO REPRESENT OTHER USERS const initBalance = new Hbar(200); const treasuryKey = PrivateKey.generateECDSA(); const [treasurySt, treasuryId] = await accountCreateFcn(treasuryKey, initBalance, client); console.log(`- Treasury's account: https://hashscan.io/testnet/account/${treasuryId}`); const aliceKey = PrivateKey.generateECDSA(); const [aliceSt, aliceId] = await accountCreateFcn(aliceKey, initBalance, client); console.log(`- Alice's account: https://hashscan.io/testnet/account/${aliceId}`); const bobKey = PrivateKey.generateECDSA(); const [bobSt, bobId] = await accountCreateFcn(bobKey, initBalance, client); console.log(`- Bob's account: https://hashscan.io/testnet/account/${bobId}`); // GENERATE KEYS TO MANAGE FUNCTIONAL ASPECTS OF THE TOKEN const supplyKey = PrivateKey.generateECDSA(); const adminKey = PrivateKey.generateECDSA(); const pauseKey = PrivateKey.generateECDSA(); const freezeKey = PrivateKey.generateECDSA(); const wipeKey = PrivateKey.generateECDSA(); Copy // ACCOUNT CREATOR FUNCTION ========================================== async function accountCreateFcn(pvKey, iBal, client) { const response = await new AccountCreateTransaction() .setInitialBalance(iBal) .setKey(pvKey.publicKey) .setAlias(pvKey.publicKey.toEvmAddress()) .setMaxAutomaticTokenAssociations(10) .execute(client); const receipt = await response.getReceipt(client); return [receipt.status, receipt.accountId]; } Copy Create a Custom Fee Schedule Let’s start by defining the custom fees for the NFT. Custom fees are distributed to the specified accounts each time the token is transferred. Depending on the token type (fungible or non-fungible), you can specify a custom fee to be fixed, fractional, or a royalty. An NFT can only have fixed or royalty fees, so in this example we’ll go with a royalty fee. This enables collecting a fraction of the value exchanged for the NFT when ownership is transferred from one person to another. // DEFINE CUSTOM FEE SCHEDULE let nftCustomFee = await new CustomRoyaltyFee() .setNumerator(5) .setDenominator(10) .setFeeCollectorAccountId(treasuryId) .setFallbackFee(new CustomFixedFee().setHbarAmount(new Hbar(200))); Copy Create a Non-Fungible Token (NFT) These are the images for our NFT collection. The images and their metadata live in the InterPlanetary File System (IPFS), which provides decentralized storage. We will need the metadata when we start minting NFTs in the next section. For the metadata, we use the standard in this specification.  These are the content identifiers (CIDs) for the NFT metadata, which points to the images and below is a sample of the metadata: // IPFS CONTENT IDENTIFIERS FOR WHICH WE WILL CREATE NFTs CID = [ "ipfs://bafyreie3ichmqul4xa7e6xcy34tylbuq2vf3gnjf7c55trg3b6xyjr4bku/metadata.json", "ipfs://bafyreicfldhlnndodwm4xe7473e2gkhyu5frhsb5rvcyq5hv4jao7f6uvu/metadata.json", "ipfs://bafyreia6ow6phsk7gdef6kzlgagfamxmujp47ue7rxymo6wr2qtenrzaeu/metadata.json", "ipfs://bafyreiax45o6ardirmfk6ua37vbnlkgpvwklntlqh5z5ohsby3tk5ep7oe/metadata.json", "ipfs://bafyreia34awv7lszb7nia2yiwsbbbefdkbr3buo22wbgk3mwwmuhk2rsby/metadata.json", ]; Copy { "name": "LEAF1.jpg", "creator": "Mother Nature", "description": "Autumn", "type": "image/jpg", "format": "none", "properties": { "city": "Boston", "season": "Fall", "decade": "20's" }, "image": "ipfs://bafybeig35bheyqpi4qlnuljpok54ud753bnp62fe6jit343hv3oxhgnbfm/LEAF1.jpg" } Copy Now, let’s create the token. Use TokenCreateTransaction() to configure and set the token properties. At a minimum, this constructor requires setting a name, symbol, and treasury account ID. All other fields are optional, so if they’re not specified then default values are used. For instance, not specifying an admin key, makes a token immutable (can’t change or add properties); not specifying a supply key, makes a token supply fixed (can’t mint new or burn existing tokens); not specifying a token type, makes a token fungible; for more info on the defaults check out the documentation. After submitting the transaction to the Hedera network, you can obtain the new token ID by requesting the receipt. This token ID represents an NFT class. // CREATE NFT WITH CUSTOM FEE let nftCreate = await new TokenCreateTransaction() .setTokenName("Fall Collection") .setTokenSymbol("LEAF") .setTokenType(TokenType.NonFungibleUnique) .setDecimals(0) .setInitialSupply(0) .setTreasuryAccountId(treasuryId) .setSupplyType(TokenSupplyType.Finite) .setMaxSupply(CID.length) .setCustomFees([nftCustomFee]) .setAdminKey(adminKey) .setSupplyKey(supplyKey) .setPauseKey(pauseKey) .setFreezeKey(freezeKey) .setWipeKey(wipeKey) .freezeWith(client) .sign(treasuryKey); let nftCreateTxSign = await nftCreate.sign(adminKey); let nftCreateSubmit = await nftCreateTxSign.execute(client); let nftCreateRx = await nftCreateSubmit.getReceipt(client); let tokenId = nftCreateRx.tokenId; console.log(`Created NFT with Token ID: ${tokenId} \n`); // TOKEN QUERY TO CHECK THAT THE CUSTOM FEE SCHEDULE IS ASSOCIATED WITH NFT var tokenInfo = await new TokenInfoQuery().setTokenId(tokenId).execute(client); console.table(tokenInfo.customFees[0]); Copy Console output: Mint and Burn NFTs In the code above for the NFT creation, the decimals and initial supply must be set to zero. Once the token is created, you will have to mint each NFT using the token mint operation. Specifying a supply key during token creation is a requirement to be able to mint and burn tokens. In terms of use cases, you may want to mint new NFTs to add items to your NFT class, or you may need to burn NFTs to take a specific item out of circulation. Alternatively, if you’re working with a fungible token (like a stablecoin), you may want to mint new tokens every time there is a new deposit and burn tokens anytime that someone converts their tokens back into fiat. In this case we’re creating a batch of five NFTs for a collection of five images. We’ll use a “token minter” function and a for loop to speed up the batch NFT minting from our array of content identifiers (CID array): // MINT NEW BATCH OF NFTs nftLeaf = []; for (var i = 0; i < CID.length; i++) { nftLeaf[i] = await tokenMinterFcn(CID[i]); console.log(`Created NFT ${tokenId} with serial: ${nftLeaf[i].serials[0].low}`); } Copy // TOKEN MINTER FUNCTION ========================================== async function tokenMinterFcn(CID) { mintTx = await new TokenMintTransaction() .setTokenId(tokenId) .setMetadata([Buffer.from(CID)]) .freezeWith(client); let mintTxSign = await mintTx.sign(supplyKey); let mintTxSubmit = await mintTxSign.execute(client); let mintRx = await mintTxSubmit.getReceipt(client); return mintRx; } Copy Console output: If you change your mind and decide that you don’t need the last NFT, then you can burn it as follows: // BURN THE LAST NFT IN THE COLLECTION let tokenBurnTx = await new TokenBurnTransaction() .setTokenId(tokenId) .setSerials([CID.length]) .freezeWith(client) .sign(supplyKey); let tokenBurnSubmit = await tokenBurnTx.execute(client); let tokenBurnRx = await tokenBurnSubmit.getReceipt(client); console.log(`\nBurn NFT with serial ${CID.length}: ${tokenBurnRx.status} \n`); var tokenInfo = await new TokenInfoQuery().setTokenId(tokenId).execute(client); console.log(`Current NFT supply: ${tokenInfo.totalSupply} \n`); Copy Console output: Auto-Associate and Transfer NFTs Before an account that is not the treasury for a token can receive or send this specific token ID, they must become “associated” with the token — this helps reduce unwanted spam and other concerns from users that don’t want to be associated with any of the variety of tokens that are created on the Hedera network. This association between an account and a token ID can be done in two ways, manually or automatically. Note that automatic associations can be done for both existing and newly created accounts. For the purposes of our example, we’ll do both. Alice’s account will be updated to associate with the token automatically Bob’s account will be manually associated with the token ID // AUTO-ASSOCIATION FOR ALICE'S ACCOUNT let associateTx = await new AccountUpdateTransaction() .setAccountId(aliceId) .setMaxAutomaticTokenAssociations(100) .freezeWith(client) .sign(aliceKey); let associateTxSubmit = await associateTx.execute(client); let associateRx = await associateTxSubmit.getReceipt(client); console.log(`Alice NFT Auto-Association: ${associateRx.status} \n`); // MANUAL ASSOCIATION FOR BOB'S ACCOUNT let associateBobTx = await new TokenAssociateTransaction() .setAccountId(bobId) .setTokenIds([tokenId]) .freezeWith(client) .sign(bobKey); let associateBobTxSubmit = await associateBobTx.execute(client); let associateBobRx = await associateBobTxSubmit.getReceipt(client); console.log(`Bob NFT Manual Association: ${associateBobRx.status} \n`); Copy Console output: Finally, let’s do two transfers of the NFT with serial number 2 and see how the royalty fees are collected. The first transfer will be from the Treasury to Alice, and the second NFT transfer will be from Alice to Bob in exchange for 100 hbar. NFT Transfer from Treasury to Alice Now, let’s do the first NFT transfer and check the account balances before and after the send. // BALANCE CHECK 1 oB = await bCheckerFcn(treasuryId); aB = await bCheckerFcn(aliceId); bB = await bCheckerFcn(bobId); console.log(`- Treasury balance: ${oB[0]} NFTs of ID:${tokenId} and ${oB[1]}`); console.log(`- Alice balance: ${aB[0]} NFTs of ID:${tokenId} and ${aB[1]}`); console.log(`- Bob balance: ${bB[0]} NFTs of ID:${tokenId} and ${bB[1]}`); // 1st TRANSFER NFT Treasury->Alice let tokenTransferTx = await new TransferTransaction() .addNftTransfer(tokenId, 2, treasuryId, aliceId) .freezeWith(client) .sign(treasuryKey); let tokenTransferSubmit = await tokenTransferTx.execute(client); let tokenTransferRx = await tokenTransferSubmit.getReceipt(client); console.log(`\n NFT transfer Treasury->Alice status: ${tokenTransferRx.status} \n`); // BALANCE CHECK 2: COPY/PASTE THE CODE ABOVE IN BALANCE CHECK 1 Copy // BALANCE CHECKER FUNCTION ========================================== async function bCheckerFcn(id) { balanceCheckTx = await new AccountBalanceQuery().setAccountId(id).execute(client); return [balanceCheckTx.tokens._map.get(tokenId.toString()), balanceCheckTx.hbars]; } Copy Console output: As you remember from the Custom Token Fees documentation, the treasury account and any fee-collecting accounts for a token are exempt from paying custom transaction fees when the token is transferred. Since the treasury account is also the fee collector for the token, that means there are no royalty fees collected in this first transfer. NFT Transfer from Alice to Bob // 2nd NFT TRANSFER NFT Alice->Bob let tokenTransferTx2 = await new TransferTransaction() .addNftTransfer(tokenId, 2, aliceId, bobId) .addHbarTransfer(aliceId, 100) .addHbarTransfer(bobId, -100) .freezeWith(client) .sign(aliceKey); tokenTransferTx2Sign = await tokenTransferTx2.sign(bobKey); let tokenTransferSubmit2 = await tokenTransferTx2Sign.execute(client); let tokenTransferRx2 = await tokenTransferSubmit2.getReceipt(client); console.log(`\n NFT transfer Alice->Bob status: ${tokenTransferRx2.status} \n`); // BALANCE CHECK 3: COPY/PASTE THE CODE ABOVE IN BALANCE CHECK 1 Copy Console output: Remember from the documentation that royalty fees are paid from the fungible value exchanged, which was 100 hbar in this case. The royalty fee is specified to be 50%, so that’s why the treasury collects 50 hbar and Alice collects the remaining 50 hbar. Keep in mind that when there’s no exchange of fungible value (like hbar or a fungible token), the fallback fee is charged (currently 200 hbar in our custom fee schedule). Conclusion You just learned how to create an NFT on the Hedera network at the native layer, without the need to code complex smart contracts! With just a few lines of code in your favorite programming language you can create, mint, burn, associate and transfer NFTs. Continue reading Part 2 to learn how to work with compliance features like know your customer (KYC), update tokens, and schedule transactions. In Part 3, you will see how to pause, freeze, wipe, and delete tokens. Code Check https://github.com/hedera-dev/hedera-example-hts-nft-blog-p1-p2-p3/blob/main/nft-part1.js Share This Back to blog What is gRPC, gRPC-Web, and Proxies? Ed Marquez Pragmatic Blockchain Design Patterns – Integrating Blockchain into Business Processes Michiel Mulders Zero Cost EthereumTransaction on Success: Hedera's New Fee Model for Relay Operators Oliver Thorn Hedera Adopts Chainlink Standard for Cross-Chain Interoperability To Accelerate Ecosystem Adoption Hedera Team Hedera Developer Highlights March 2025 Michiel Mulders Hedera Release Cycle Overview Ed Marquez View All Posts Sign up for the newsletter CONNECT WITH US Transparency Open Source Audits & Standards Sustainability Commitment Carbon Offsets Governance Hedera Council Public Policy Treasury Management Meeting Minutes LLC Agreement Node Requirements Community Events Meetups HBAR Telegram Developer Discord Twitter Community Support FAQ Network Status Developer Discord StackOverflow Brand Brand Guidelines Built on Hedera Logo Hedera Store About Team Partners Journey Roadmap Careers Contact General Inquiry Public Relations © 2018-2025 Hedera Hashgraph, LLC. All trademarks and company names are the property of their respective owners. All rights in the Deutsche Telekom mark are protected by Deutsche Telekom AG. All rights reserved. Hedera uses the third party marks with permission. Terms of Use  |  Privacy Policy