Mint an NFT end-to-end
This guide walks the complete flow from nothing to a minted token, using only the SDK. Every step is copy-pasteable against api.highlightv2.xyz on a testnet chain.
For a quick-fire first call, use the developer quick start. Come back here when you need to see the whole pipeline.
What you’ll build
Section titled “What you’ll build”A public, fixed-price Open Edition on Base Sepolia, with one image, deployed end-to-end, ready for a collector to claim. ~100 lines of code.
Prerequisites
Section titled “Prerequisites”- Node 20+ or Bun 1.1+
- A wallet (viem
Account) with some testnet ETH on your target chain - The API base URL:
https://api.highlightv2.xyz
bun add @highlightxyz/sdk viem1. Sign in
Section titled “1. Sign in”The full auth details live in reference/auth. Short version: SIWE gives you a 15-minute JWT.
import { createHighlightClient, MediaKind } from "@highlightxyz/sdk";import { createSiweMessage } from "viem/siwe";import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);const baseUrl = "https://api.highlightv2.xyz";
const anon = createHighlightClient({ baseUrl });
const { nonce } = (await anon.user.siwe.nonce()).data!;
const message = createSiweMessage({ address: account.address, chainId: 1, domain: new URL(baseUrl).host, nonce, uri: baseUrl, version: "1", issuedAt: new Date(), expirationTime: new Date(Date.now() + 10 * 60 * 1000),});const signature = await account.signMessage({ message });
const { tokens } = (await anon.user.signin({ body: { providerType: "Siwe", message, signature },})).data!;
const client = createHighlightClient({ baseUrl, auth: () => tokens.accessToken, security: [{ type: "http", scheme: "bearer" }],});From here on, client is authenticated.
2. Upload the artwork
Section titled “2. Upload the artwork”Media upload is a three-step dance: create a session, PUT bytes, poll until Ready. See media upload flow for the full reference.
import { readFileSync } from "node:fs";
const buffer = readFileSync("./artwork.png");
const session = (await client.media.createUploadSession({ kind: MediaKind.File, fileName: "artwork.png", mimeType: "image/png", fileSize: buffer.byteLength,})).data!;
// PUT the bytes. The upload URL is a worker endpoint on the same origin.await fetch(session.upload.url, { method: session.upload.method, headers: { ...session.upload.headers, authorization: `Bearer ${tokens.accessToken}`, }, body: buffer,});
// Poll until Readyconst logoMediaId = session.media.id;let status = "Pending";while (status !== "Ready") { await new Promise((r) => setTimeout(r, 2000)); status = (await client.media.get({ mediaId: logoMediaId })).data!.status; if (status === "Failed") throw new Error("Media processing failed");}3. Create the draft collection
Section titled “3. Create the draft collection”The collection starts in Draft. We’ll bind it to a brand-new contract on Base Sepolia (chain 84532).
const collection = (await client.collection.create({ name: "My First Drop", description: "An open edition minted via the Highlight SDK.", type: "OpenEdition", logoMediaId, image: session.media.url!, contract: { chainId: 84532, name: "MYDROP", symbol: "MYDROP", standard: "ERC721", },})).data!;
const highlightId = collection.highlightId;4. Fill in edition-specific details
Section titled “4. Fill in edition-specific details”Each collection type has its own PATCH endpoint for type-specific fields. For editions:
await client.collection.updateEditionDetails({ highlightId, edition: { size: 0, // 0 = open supply name: "My First Drop", description: "An open edition minted via the Highlight SDK.", imageMediaId: logoMediaId, attributes: [{ trait_type: "Series", value: "Genesis" }], },});5. Configure the sale
Section titled “5. Configure the sale”A FIXED_PRICE / PUBLIC sale — the simplest supported combination. See the support matrix for everything else.
const sale = (await client.collection.addSale({ highlightId, type: "FIXED_PRICE", accessMode: "PUBLIC", startAt: new Date().toISOString(), price: "0.001", currency: "ETH", maxPerTransaction: 5, maxPerWallet: 10, maxTotal: 1000, paymentRecipient: account.address,})).data!;6. Deploy the contract
Section titled “6. Deploy the contract”Deployment is an async workflow. Kick it off, then poll until it needs a wallet signature. See deploy workflow for the full state machine.
import { createWalletClient, http } from "viem";import { baseSepolia } from "viem/chains";
const walletClient = createWalletClient({ account, chain: baseSepolia, transport: http(),});
await client.collection.deploy({ highlightId });
// Poll until the workflow has a tx ready for us to signlet deployState = (await client.collection.deployStatus({ highlightId })).data!;while (deployState.currentStep !== "AwaitTransaction" && deployState.status !== "Completed") { if (deployState.status === "Failed") throw new Error(deployState.error ?? "deploy failed"); await new Promise((r) => setTimeout(r, 3000)); deployState = (await client.collection.deployStatus({ highlightId })).data!;}
// Sign & submit the deployment transaction.// `contractConfig.to` is null for contract-creation transactions — pass undefined to viem.const cfg = deployState.contractConfig!;const hash = await walletClient.sendTransaction({ to: (cfg.to ?? undefined) as `0x${string}` | undefined, data: cfg.data as `0x${string}`, value: BigInt(cfg.value ?? "0"), gas: 5_000_000n,});
// Tell Highlight the txHash; the indexer picks it up from thereawait client.collection.deployConfirm({ highlightId, txHash: hash });
// Wait for the collection to go Livewhile ((await client.collection.get({ highlightId })).data!.status !== "Live") { await new Promise((r) => setTimeout(r, 5000));}7. Let a collector mint
Section titled “7. Let a collector mint”From the collector’s perspective, minting is a single SDK call that returns a contract execution to submit on-chain. For a public fixed-price sale, no executor signature is involved.
const claim = (await client.collection.claimSale({ highlightId, saleId: sale.id, amount: 1,})).data!;// claim is { chainId, to, data, value } — a flat contract-execution shape.
const mintHash = await walletClient.sendTransaction({ to: (claim.to ?? undefined) as `0x${string}` | undefined, data: claim.data as `0x${string}`, value: BigInt(claim.value ?? "0"), gas: 500_000n,});Once the transaction confirms, the indexer picks up the Transfer event, and a Token record appears on the collection.
const tokens = (await client.token.list({ highlightId, limit: 10 })).data!;console.log(`${tokens.pagination.total} tokens minted so far`);What to learn next
Section titled “What to learn next”- Gated sales — require an allowlist or token ownership. See gated sales.
- Dutch auctions — scheduled price drops. See sale types.
- Ranked auctions — sealed-bid, on-chain. See ranked auctions.
- Series with unique tokens — upload a zip, let collectors choose. See series.
- Generative — deploy HTML/JS code that renders at mint time. See generative.