Developers

Dynamic Minting API

Mint Chia NFTs from your own app with one API call. Upload an image, choose who receives the NFT, and MintGarden builds the metadata, stores the files on IPFS and mints on-chain. Our Secure the Mint technology links every NFT to your creator profile for provenance. Each mint uses one prepaid mint credit.

Get started

  1. 1

    Create a creator profile

    Every NFT you mint is linked to a creator profile (a DID), so collectors can verify who made it.

    Create a profile →
  2. 2

    Buy mint credits

    One credit mints one NFT. Credits are created on-chain for your profile and are not transferable.

    Buy credits →
  3. 3

    Create an API key

    Your first credit purchase creates one for you. Send it as a bearer token with every request. The key identifies your profile, so keep it on your server.

    Create a key →
You are on testnet. Credits are free here; you only pay the network fee in testnet XCH. Run the whole flow before you go to mainnet.

Your dashboard

Buy mint credits, create and revoke API keys, check recent mints and your webhook, and run a test mint from your browser.

Open the dashboard

Mint your first NFT

Export your API key as MINTGARDEN_API_KEY. Every example below is complete: copy it, replace the addresses with your own, and run it. The curl examples need jq, the JavaScript examples run on Node 18 or later as an .mjs file, and the Python examples need requests.

Simple mode: upload an image

Describe the NFT and MintGarden builds the CHIP-0007 metadata, stores both files on IPFS and computes the hashes. Reserve an upload with POST /mint/dynamic/uploads, PUT exactly size_bytes bytes to the returned upload_url with the returned upload_headers before expires_at, then mint it with nft. The example uploads gm-yak.png from the current directory and mints it to target_address. The NFT links the image and its metadata as https://ipfs.mintgarden.io/ipfs/{cid} and ipfs://{cid}.

API=https://api.testnet.mintgarden.io

# 1. Reserve an upload for the image
UPLOAD=$(curl -sS --fail-with-body -X POST "$API/mint/dynamic/uploads" \
  -H "Authorization: Bearer $MINTGARDEN_API_KEY" \
  -H "Content-Type: application/json" \
  --data @- <<EOF
{
  "filename": "gm-yak.png",
  "content_type": "image/png",
  "size_bytes": $(wc -c < gm-yak.png)
}
EOF
)
UPLOAD_ID=$(echo "$UPLOAD" | jq -r .upload_id)

# 2. PUT the file to the returned URL with the returned headers
echo "$UPLOAD" | jq -r '.upload_headers | to_entries[] | "header = \"\(.key): \(.value)\""' \
  | curl -sS --fail-with-body --upload-file gm-yak.png -K - "$(echo "$UPLOAD" | jq -r .upload_url)"

# 3. Mint the NFT
curl -sS --fail-with-body -X POST "$API/mint/dynamic" \
  -H "Authorization: Bearer $MINTGARDEN_API_KEY" \
  -H "Content-Type: application/json" \
  --data @- <<EOF
{
  "nft": {
    "name": "GM Yak #1",
    "description": "A yak saying good morning.",
    "attributes": [
      {
        "trait_type": "Background",
        "value": "Mint"
      }
    ],
    "image": {
      "upload_id": "$UPLOAD_ID"
    }
  },
  "target_address": "txch1ysveu6vh0evhuwq30708syfep7nfaed29l0ejee9nw3wjfjalq8scw4ydh",
  "royalty_address": "txch1ysveu6vh0evhuwq30708syfep7nfaed29l0ejee9nw3wjfjalq8scw4ydh",
  "royalty_percentage": 5
}
EOF

Instead of an upload you can pass a public image URL as "image": {"url": "https://…"}. MintGarden fetches it once to hash it, up to 25 MB, and the NFT links that URL. The server must answer directly, without a redirect or a compressed body.

Raw mode: bring your own metadata

If you host the files yourself, send metadata with their URIs and sha256 hashes. MintGarden mints exactly what you send.

API=https://api.testnet.mintgarden.io

curl -sS --fail-with-body -X POST "$API/mint/dynamic" \
  -H "Authorization: Bearer $MINTGARDEN_API_KEY" \
  -H "Content-Type: application/json" \
  --data @- <<EOF
{
  "metadata": {
    "data_uris": [
      "https://ipfs.mintgarden.io/ipfs/bafybeidcvjjsqfzae37mzpn7njxnlgdtspya2wkrrkaijoyngux5cjth3m/gm-yak.webp"
    ],
    "data_hash": "8e66307cc9eb7cd8d00d0ffdf2cc4bdde0b415921f850b44c070b43291522a2a",
    "metadata_uris": [
      "https://ipfs.mintgarden.io/ipfs/bafybeidcvjjsqfzae37mzpn7njxnlgdtspya2wkrrkaijoyngux5cjth3m/metadata.json"
    ],
    "metadata_hash": "b9d6f7898cf79169ab31cd2bf5d4f773c27380da3c2b6c70cdecf5737028b7b3",
    "edition_number": 1,
    "edition_total": 1
  },
  "target_address": "txch1ysveu6vh0evhuwq30708syfep7nfaed29l0ejee9nw3wjfjalq8scw4ydh",
  "royalty_address": "txch1ysveu6vh0evhuwq30708syfep7nfaed29l0ejee9nw3wjfjalq8scw4ydh",
  "royalty_percentage": 5
}
EOF

Mint offers: sell the mint

Add requested_payments and the API returns an offer instead of minting right away. Whoever takes the offer pays and receives the NFT; the payment goes to target_address. Amounts are in mojos: 1 XCH is 1000000000000 mojos and 1 unit of a CAT is 1000 mojos. Set asset_id to null for XCH or to the CAT's asset id, and request each asset once. reserve_for_seconds lets the offer expire and frees its credit; without it the offer stays open until taken. publish_offer lists the offer on MintGarden.

1 XCH
API=https://api.testnet.mintgarden.io

curl -sS --fail-with-body -X POST "$API/mint/dynamic" \
  -H "Authorization: Bearer $MINTGARDEN_API_KEY" \
  -H "Content-Type: application/json" \
  --data @- <<EOF
{
  "metadata": {
    "data_uris": [
      "https://ipfs.mintgarden.io/ipfs/bafybeidcvjjsqfzae37mzpn7njxnlgdtspya2wkrrkaijoyngux5cjth3m/gm-yak.webp"
    ],
    "data_hash": "8e66307cc9eb7cd8d00d0ffdf2cc4bdde0b415921f850b44c070b43291522a2a",
    "metadata_uris": [
      "https://ipfs.mintgarden.io/ipfs/bafybeidcvjjsqfzae37mzpn7njxnlgdtspya2wkrrkaijoyngux5cjth3m/metadata.json"
    ],
    "metadata_hash": "b9d6f7898cf79169ab31cd2bf5d4f773c27380da3c2b6c70cdecf5737028b7b3",
    "edition_number": 1,
    "edition_total": 1
  },
  "target_address": "txch1ysveu6vh0evhuwq30708syfep7nfaed29l0ejee9nw3wjfjalq8scw4ydh",
  "royalty_address": "txch1ysveu6vh0evhuwq30708syfep7nfaed29l0ejee9nw3wjfjalq8scw4ydh",
  "royalty_percentage": 5,
  "requested_payments": [
    {
      "asset_id": null,
      "amount": 1000000000000
    }
  ],
  "reserve_for_seconds": 3600,
  "publish_offer": true
}
EOF
1 SBX (a CAT)
API=https://api.testnet.mintgarden.io

curl -sS --fail-with-body -X POST "$API/mint/dynamic" \
  -H "Authorization: Bearer $MINTGARDEN_API_KEY" \
  -H "Content-Type: application/json" \
  --data @- <<EOF
{
  "metadata": {
    "data_uris": [
      "https://ipfs.mintgarden.io/ipfs/bafybeidcvjjsqfzae37mzpn7njxnlgdtspya2wkrrkaijoyngux5cjth3m/gm-yak.webp"
    ],
    "data_hash": "8e66307cc9eb7cd8d00d0ffdf2cc4bdde0b415921f850b44c070b43291522a2a",
    "metadata_uris": [
      "https://ipfs.mintgarden.io/ipfs/bafybeidcvjjsqfzae37mzpn7njxnlgdtspya2wkrrkaijoyngux5cjth3m/metadata.json"
    ],
    "metadata_hash": "b9d6f7898cf79169ab31cd2bf5d4f773c27380da3c2b6c70cdecf5737028b7b3",
    "edition_number": 1,
    "edition_total": 1
  },
  "target_address": "txch1ysveu6vh0evhuwq30708syfep7nfaed29l0ejee9nw3wjfjalq8scw4ydh",
  "royalty_address": "txch1ysveu6vh0evhuwq30708syfep7nfaed29l0ejee9nw3wjfjalq8scw4ydh",
  "royalty_percentage": 5,
  "requested_payments": [
    {
      "asset_id": "a628c1c2c6fcb74d53746157e438e108eab5c0bb3e5c80ff9b1910b3e4832913",
      "amount": 1000
    }
  ],
  "reserve_for_seconds": 3600,
  "publish_offer": true
}
EOF

Field reference

POST /mint/dynamic takes a JSON body. Send either nft or metadata, never both. In simple mode the metadata MintGarden builds may be up to 1 MB.

DynamicMintInput

FieldTypeDescription
nft
nft or metadata
SimpleNftSimple mode. Describe the NFT and MintGarden builds and stores the metadata.
metadata
nft or metadata
MetadataListEntryRaw mode. URIs and sha256 hashes of files you host yourself.
target_address
Required
stringDirect mint: the address that receives the NFT. Mint offer: the address that receives the payment.
royalty_address
Required
stringThe address that receives royalties.
royalty_percentage
Required
numberRoyalty on resales in percent, from 0 to 100, for example 5 for 5%. Rounded down to two decimals.
requested_paymentsRequestedPayment[]Create a mint offer that asks for these payments, at most one per asset. Omit it or send [] to mint directly.
reserve_for_secondsintegerMint offers only: the offer expires after this many seconds and its credit can mint something else. Omit it, or send null or 0, and the offer stays open until taken. Must not be negative. Ignored for a direct mint, whose credit is held with no expiry.
publish_offerbooleanMint offers: list the offer publicly on MintGarden. Defaults to false.
deduplication_keystringUp to 128 characters, unique per profile. Makes retries safe, see Idempotency.
profile_idstringYour profile as did:chia:1… or hex. Optional, as the API key identifies the profile; when sent it must match.
requested_mojos
Deprecated
integerUse requested_payments instead. Cannot be combined with requested_payments.
requested_asset_id
Deprecated
stringUse requested_payments instead.

SimpleNft

FieldTypeDescription
name
Required
stringThe NFT name. Up to 256 characters.
descriptionstringThe NFT description. Up to 10 000 characters.
attributesNftAttribute[]Traits, each with trait_type and value. Up to 200.
image
Required
ImageSourceExactly one of upload_id or url.
collectionCollectionInfoThe collection this NFT belongs to.
sensitive_contentbooleanMarks the NFT as sensitive. Defaults to false.
license_urlstring (URL)A license document. MintGarden fetches it once to hash it, up to 1 MB, with the same rules as an image URL, and the NFT links this URL.
edition_numberintegerEdition number, from 1 to edition_total. Send it together with edition_total or not at all.
edition_totalintegerTotal editions, at least 1.

ImageSource

FieldTypeDescription
upload_idstringThe upload_id from POST /mint/dynamic/uploads.
urlstring (URL)A public image URL. MintGarden fetches it once to hash it, up to 25 MB, and the NFT links this URL. It must answer 2xx from a public address, without a redirect or a compressed body.

NftAttribute

FieldTypeDescription
trait_type
Required
stringThe trait name, for example Background. Up to 256 characters.
value
Required
string | numberThe trait value. A string is up to 256 characters.

CollectionInfo

FieldTypeDescription
id
Required
stringThe CHIP-0007 collection id, a UUID.
name
Required
stringThe collection name. Up to 256 characters.
attributesNftAttribute[]Collection attributes such as website or twitter. Up to 200.

RequestedPayment

FieldTypeDescription
asset_id
Required
string | nullA CAT asset id, 64 hex characters without 0x, or null for XCH.
amount
Required
integerIn mojos, 0 or more. 1 XCH = 1000000000000 mojos, 1 CAT unit = 1000 mojos.

MetadataListEntry (raw mode)

FieldTypeDescription
data_uris
Required
string[]Where the NFT file is hosted.
data_hash
Required
stringsha256 of the NFT file, in hex.
metadata_uris
Required
string[]Where the CHIP-0007 JSON is hosted.
metadata_hash
Required
stringsha256 of the metadata file, in hex.
license_urisstring[]Where the license is hosted.
license_hashstringsha256 of the license file, in hex.
edition_numberintegerDefaults to 1.
edition_totalintegerDefaults to 1.

POST /mint/dynamic/uploads

FieldTypeDescription
filename
Required
stringThe file name, for example gm-yak.png.
content_type
Required
stringThe MIME type, for example image/png.
size_bytes
Required
integerThe exact file size, up to 100 MB and within your storage quota. The uploaded file must match it.

Responses

POST /mint/dynamic returns 200 once the mint is on its way. A direct mint is already in the mempool; a mint offer is ready to share. Errors return a JSON body with detail and code, see Errors.

DynamicMintResponse

FieldTypeDescription
nft_idstringThe launcher id in hex. Use it to look up the mint status.
offer_stringstring | nullMint offers: the offer1… string to share. Otherwise null.
offerobject | nullMint offers: the offer as listed on MintGarden. Otherwise null.
Direct mint response
{
  "nft_id": "5b8f3c0e8a4f4c7b9e0a2d1c6f7e8d9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e",
  "offer": null,
  "offer_string": null
}

Upload response

FieldTypeDescription
upload_idstringPass it as nft.image.upload_id.
upload_urlstringPUT the file bytes here.
upload_headersobjectSend these headers with the PUT, unchanged.
expires_atdatetimeWhen upload_url stops accepting the PUT.

Mint status

GET /mint/dynamic/{nft_id} returns the status of an NFT you minted with the API. It accepts the nft_id from the mint response or the nft1… id. Poll it every few seconds, or let a webhook tell you when the mint is confirmed.

The nft_id is fixed by the credit. When a credit is used again, for example after its offer expired, the new mint gets the same nft_id, and the lookup answers for the request that holds the credit. reserved_until is set while the mint is pending or offered and has an expiry, and confirmed_at_height once it is confirmed.

curl -sS --fail-with-body "https://api.testnet.mintgarden.io/mint/dynamic/$NFT_ID" \
  -H "Authorization: Bearer $MINTGARDEN_API_KEY"
StatusMeaning
submittingThe API call is still running. It becomes pending, offered or failed when the call returns.
pendingA direct mint was sent to the network and waits to be included in a block.
offeredA mint offer was created and waits for someone to take it, until reserved_until when that is set.
confirmedSeen in a block. MintGarden does not revert this status after a chain reorganization; wait for more blocks after confirmed_at_height if you need finality. Final.
expiredThe reservation ran out, or another request now holds the credit. This attempt is not expected to mint, but an offer taken just before the deadline can still turn confirmed once its block is indexed.
failedThe API call failed or was interrupted, and the request no longer holds a credit. After chain_submit_failed or lease_lost its spend can still land while this status stays failed, so check the NFT page before you retry. A retry with the same deduplication_key runs as a new mint. Final.
MintStatus
{
  "launcher_id": "5b8f3c0e8a4f4c7b9e0a2d1c6f7e8d9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e",
  "encoded_id": "nft1tw8ncr52fax8h8s295wx7l5dng93ctf7fadxklydnc8352euf40q4ac9zk",
  "status": "confirmed",
  "mode": "direct",
  "deduplication_key": "gm-yak-1",
  "created_at": "2026-10-25T12:00:00Z",
  "confirmed_at_height": 6512345,
  "reserved_until": null,
  "offer_string": null,
  "url": "https://mintgarden.io/nfts/nft1tw8ncr52fax8h8s295wx7l5dng93ctf7fadxklydnc8352euf40q4ac9zk"
}

Idempotency

Network errors happen. Send a deduplication_key (up to 128 characters, unique per profile) and you can retry a mint without minting twice or spending a second credit.

  • Same key, same body: 200 with the stored response and the header Idempotent-Replayed: true. Only server-side code sees this header: the API does not expose it to browsers through CORS.
  • Same key, different body (profile_id aside): 422 with code deduplication_key_reused.
  • Same key while the first call is still running: 409 with code deduplication_in_progress. Wait a moment and retry.
  • Same key after the first call failed, or after it stopped running without an answer for 10 minutes: the retry runs as a new mint. After chain_submit_failed or lease_lost the first spend can still land, so check the NFT first, see Errors.

Webhooks

Instead of polling, MintGarden can call your server when a mint is confirmed. Set one HTTPS URL per profile with PUT /mint/dynamic/webhook. It returns url, created_at and a secret that is shown only once; every PUT rotates it. The URL must be https, at most 2048 characters, and its host must resolve to a public address within 5 seconds, or the call answers 422 with invalid_request.

GET shows the current url and created_at, and DELETE removes the webhook along with every delivery still waiting. Without a webhook both answer 404 with webhook_not_configured. POST /mint/dynamic/webhook/test queues a ping and answers 202 with its delivery_id. While that ping is still waiting, another test answers 200 with the same delivery_id and sends it right away.

curl
curl -sS --fail-with-body -X PUT "https://api.testnet.mintgarden.io/mint/dynamic/webhook" \
  -H "Authorization: Bearer $MINTGARDEN_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"url": "https://example.com/mintgarden/webhook"}'

# Send a test "ping" delivery
curl -sS --fail-with-body -X POST "https://api.testnet.mintgarden.io/mint/dynamic/webhook/test" \
  -H "Authorization: Bearer $MINTGARDEN_API_KEY"

Events

  • mint.confirmed: the mint was seen in a block. It is sent once per NFT, and data is the MintStatus of the request that confirmed the credit: the request holding the credit if it was submitted, otherwise the newest submitted request for that NFT.
  • ping: a test delivery. data is {"profile_id": "…"}.
Delivery
POST https://example.com/mintgarden/webhook
Content-Type: application/json
MintGarden-Signature: t=1792900800,v1=5f0c1e…
MintGarden-Event: mint.confirmed
MintGarden-Delivery: 3f9a2c64-1d0e-4c5b-9a7e-2b8d6f1e0c3a

{
  "id": "3f9a2c64-1d0e-4c5b-9a7e-2b8d6f1e0c3a",
  "type": "mint.confirmed",
  "created_at": "2026-10-25T12:01:30Z",
  "data": { "launcher_id": "…", "encoded_id": "nft1…", "status": "confirmed", … }
}

Verify the signature

MintGarden-Signature has the form t=<unix seconds>,v1=<signature>, where the signature is the hex HMAC-SHA256 of <t>.<raw body> keyed with your webhook secret. Compute it over the raw request body, compare in constant time, and reject old timestamps so a captured request cannot be replayed.

import crypto from 'node:crypto';

// rawBody is the exact request body string, before any JSON parsing.
export function verifyMintGardenSignature(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(signatureHeader.split(',').map((part) => part.split('=', 2)));
  const timestamp = Number(parts.t);
  if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest();
  const received = Buffer.from(parts.v1 ?? '', 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

Delivery and retries

  • Respond with any 2xx within 10 seconds. Anything else counts as a failure, and so does a redirect, which is not followed.
  • Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, then every 12 hours. After 10 attempts in total the delivery is given up.
  • Delivery is at least once, so the same event can arrive more than once. Use id (the same as MintGarden-Delivery) to ignore repeats.
  • mint.confirmed fires on the first confirmation and is not retracted after a chain reorganization, and the confirmed status is not reverted either. If you need finality, wait for more blocks after confirmed_at_height.

Errors

Errors return a JSON body with a human readable detail and a stable code. Branch on code, not on the message.

Errors raised after the mint was sent, lease_lost and chain_submit_failed, add nft_id next to code. Do not retry them automatically: the spend may still land. Wait a minute, then look at GET /mint/dynamic/{nft_id} and the NFT page /nfts/{nft_id}. The request itself stays failed, so the NFT page is where a landed spend shows up. A retry with the same deduplication_key replaces the failed attempt and mints a second NFT if the first spend lands.

A body that does not match the schema, such as a missing field or a string over its length limit, gets 422 with code invalid_request. Its detail is then a list of field errors, each with loc (the path to the field) and msg.

Error response
{
  "detail": "No available credits.",
  "code": "no_credits"
}
CodeHTTPWhen
missing_api_key401No Authorization: Bearer header.
invalid_api_key401The key is unknown or revoked.
profile_mismatch403The profile_id in the body is not the profile of the key.
minting_blocked403The profile is blocked from minting. Every endpoint that takes an API key answers this.
no_credits402The profile has no unspent credit. Buy more in the dashboard.
invalid_request422The body is invalid: both or neither of nft and metadata, both or neither of image.upload_id and image.url, both payment styles, the same asset requested twice, a bad address, asset id or amount, a royalty outside 0 to 100, a negative reserve_for_seconds, editions out of range, metadata over 1 MB, or a webhook URL that is not a public https address.
deduplication_key_reused422The deduplication_key was used for a different request.
deduplication_in_progress409The first request with this key is still running.
lease_lost409Another request took this credit before the mint was recorded, and this request failed. Its spend can still land. Not safe to retry right away, see below.
mint_not_found404Status lookup for an NFT this profile did not mint with the API.
upload_not_found404The upload_id is unknown or belongs to another profile.
upload_not_ready409The file is not in storage yet, storage has not pinned it yet, or another mint of the same upload is in progress. Retry shortly.
upload_too_large413The upload is over 100 MB or the storage quota, or the uploaded file is not the size_bytes reserved for it.
fetch_failed422An image or license URL was refused, unreachable, answered with a redirect or an error, sent a compressed body, or is over its size limit. Not retried automatically: retry only if you know the host was briefly unreachable.
chain_submit_failed502Sending the mint to the network failed, and MintGarden could not confirm that the network has the spend. The credit was released, but the spend can still land. Not safe to retry right away, see below.
offer_upload_failedvariesThe mint offer could not be stored. The status and detail are those of the offer upload. The credit was released. Retry when the status is 5xx.
webhook_not_configured404Webhook read, test or delete without a webhook.

API reference

The OpenAPI reference lists every endpoint and schema of the Dynamic Minting API: https://api.testnet.mintgarden.io/docs#tag/Dynamic-Minting.

MintGarden

Explore and mint NFTs in the Chia ecosystem.

Settings

This platform provides access to user-submitted NFT listings and tools for peer-to-peer interaction. We do not operate as a regulated financial service provider under MiCAR, MiFID II, or similar laws. The inclusion of NFTs on this site does not imply any endorsement or legal classification. NFTs may carry financial risks and may be subject to regulatory restrictions in your country. Use at your own risk.

© 2026 MintGarden. All rights reserved.

Testnet