ORD20ORD20 APIapi.ord20.ioapi.ordnet.io →ord20.io →
Read API · Poort I-6

Everything the index knows about ORD20, as JSON.

Tokens, cards, mint state, balances per address, transfers, holders, listings and trades — read from the ORD20 indexer on the ORDnet node. One write route: broadcast a transaction through our own node.

Base URLhttps://api.ord20.io/v1/ord20
numbers
always strings
counters are u64/i128; parse with BigInt, never Number
identity
by id, never by symbol
72 hex chars: deploy txid (little-endian) ‖ vout (LE u32)
errors
{ "error": { code, message } }
not_found · invalid_id · invalid_request · broadcast_rejected · node_unavailable · internal

Conventions

Reads are served from one repeatable-read snapshot per request, so the numbers in a single answer belong together. No authentication. CORS is open to ord20.io origins.

TermMeaning
idToken id: the deploy transaction's txid in little-endian hex followed by the card output index as little-endian u32 — 72 lowercase hex characters. Symbols (sym) are labels and can repeat.
outpointSame 72-hex form (txid LE ‖ vout LE) for any output, e.g. the active mint state.
pkh / addressInput addresses are given as the 40-hex public-key hash. Output rows carry both: the base58 P2PKH address for display (address, from, to, seller, buyer) and the pkh next to it (*_pkh).
unitsOne unit is one satoshi locked in the token covenant. Display convention: two decimals (1.00 = 100 units).
heightBlock height as a string. tx_index is the transaction's position in that block.

Routes

The first seven routes are the contract of the mint-page specification §7; the last five are the website additions v1.0 (explorer, token page, market, richlist).

GET/status

Index tip, node tip and how far the index is behind; counters and the sync profile (anchor block 963369, ruleset 0.2.18, mainnet).

{ "tip": { "height": "967017", "hash": "00000000…" }, "node_tip": "967020", "index_behind": "3", "index_behind_threshold": "3",
  "blocks": "3649", "tokens": "1", "utxos": "3", "reorgs": "0", "catalog_hash": "…",
  "sync_profile": { "start_height": "963369", "start_parent_hash": "…", "ruleset": "0.2.18", "network": "main:…", "anchor_id": "…" },
  "indexer_node_height": "967020", "node_error": null,
  "node_tip_time": "1789600000", "node_tip_age_s": "312", "index_in_sync": true,
  "api_version": "1.4.0", "api_uptime_s": "86400", "server_time": "1789600312" }

index_in_sync is index_behind ≤ index_behind_threshold; node_tip_time/node_tip_age_s come from the node's block header, so a large age means the network has not found a block for a while, not that anything here is broken.

GET/tokens?mintable=1&q=&page=

mintable=1 keeps only tokens whose mint is open with an active state. q matches the symbol (case-insensitive) or an id prefix. 50 per page; has_more tells you to fetch the next.

{ "items": [ {
    "id": "aa9f…00000000", "card": { "p": "ord-20", "op": "deploy", "ver": 1, "sym": "ORDTEST", "name": "ORD20 network test",
      "cap_mode": "capped", "cap": "10000", "premint": "1000", "m1_enabled": true, "x": "1000", "mint_fee": "100",
      "fee_default": "token", "mint_vout": 1, "whitelist_root": null },
    "deploy_height": "967006", "deploy_txid": "829f…", "holders": "2", "issuer_pkh": "e8e5…", "issuer_address": "1NETFY…",
    "mint_status": "open", "minted_total": "2000", "free": "1807", "listed": "0", "loss_a": "193", "loss_c": "0", "invalidated": "0",
    "active_mint": "3176…00000000",
    "active_mint_state": { "ver": 1, "id": "aa9f…", "issuer": "e8e5…", "cap_mode": "capped", "cap": "10000", "x": "1000",
      "mint_fee": "100", "m1": true, "premint": "1000", "activated": true, "remaining": "8000", "minted_total": "2000" },
    "last_height": "967014" } ],
  "page": "1", "page_size": "50", "has_more": false }
GET/tokens/{id}

One token record, same shape as an item above. Unknown id → 404 not_found; anything that is not 72 hex chars → 400 invalid_id.

GET/tokens/{id}/mint-state

The active mint state to build an M1 against. outpoint is the state UTXO; active_mint_state is null when the mint is closed or not activated.

{ "outpoint": "3176…00000000", "mint_status": "open", "height": "967013",
  "active_mint_state": { "id": "…", "issuer": "…", "cap_mode": "capped", "cap": "10000", "x": "1000", "mint_fee": "100", "m1": true,
                         "premint": "1000", "activated": true, "remaining": "8000", "minted_total": "2000" } }
GET/tokens/{id}/mint-head

Since 1.5.1. The mint state to build a mint against right now: the confirmed state from the index, followed through every unconfirmed mint in the node's mempool to the current head. Wallets must build on head, not on /mint-state (which only moves with blocks) — that is what lets many wallets mint in the same block. Never cached (Cache-Control: no-store); an observation, not a reservation. status is usable, not_activated, cap_exhausted, closed, state_lost or unresolved (with reason and retry_after_ms; then head is null and nothing may be built). /mint-head/chain adds every unconfirmed link.

{ "status": "usable", "complete": true, "depth_unconfirmed": "2",
  "base": { "height": "967298", "outpoint": "…00000000" },
  "head": { "txid": "…", "vout": 0, "script_hex": "…", "confirmed": false },
  "state": { "remaining": "500", "minted_total": "9500", "x": "1000", "mint_fee": "100", "m1": true, … } }
GET/tokens/{id}/icon

The token's icon as an image (since 1.2.0). The bytes are read from the deploy transaction on the ORDnet node: output card.icon_vout is an ordinary inscription (OP_FALSE OP_IF "ord" OP_1 <content-type> OP_0 <data> OP_ENDIF). Served with the inscription's content type (png, jpeg, gif, webp or svg) and Cache-Control: immutable — the deploy cannot change. 404 when the card has no icon_vout; 415 for other content types. Since 1.4.0 a token without icon_vout may get an off-chain courtesy icon kept by ORDnet (response header X-ORD20-Icon-Source: off-chain, one-hour cache) — this is presentation only and not part of the token; on-chain icons carry X-ORD20-Icon-Source: deploy-inscription.

GET /v1/ord20/tokens/576b15946f60…00000000/icon  →  200 image/png (29 491 bytes)

Market (since 1.4.0)

The marketplace data layer, derived entirely from the indexer's lossless history (events, outputs, spends). Prices per unit are sats per token-sat with 8 decimals; per token = ×100. A token that has never traded carries the backing as reference price (1 token = 100 sats, price_source: "backing"); last_price fields stay empty until a confirmed sale. Times are block-header times from the node. All read routes answer with Access-Control-Allow-Origin: * and a short public cache, so third parties (CoinMarketCap, CoinGecko, wallets) can consume them directly. Full schema: /openapi.json.

GET/market/listings · /market/listings/{outpoint} · /market/trades · /market/tokens · /market/tokens/{id}/candles · /market/tokens/{id}/depth · /market/stats

Open listings across all tokens (filters token, seller, price bounds; sort by price per unit, height or units), one listing with its history (open | sold | cancelled), all sales with buyer and seller, the market table per token (floor, last price, reference price, market cap on living supply, volumes per 24 h / 7 d / 30 d / all, holders), OHLCV candles from sales (interval=1h|1d, bucketed by blocks), the ask side of the book, and global totals. Also /address/{pkh}/listings and /address/{pkh}/trades.

GET/public/assets · /public/tickers · /public/summary · /public/orderbook?ticker_id= · /public/historical_trades?ticker_id=

Exchange-integration forms. Ticker ids are <SYM>_<first 12 hex of id>_BSV; prices in BSV per token; backing_price is always present, last_price only after a sale. Fees: maker 0, taker 0.33 % (enforced on-chain from lock v2).

GET/price

BSV/USD reference rate fetched server-side (WhatsOnChain), cached 60 s, with stale: true when the source is unreachable; usd_per_token_backing is the USD value of 100 sats.

GET/funding/{address}

Since 1.4.0. Plain P2PKH outputs of a mainnet address, for paying mints and transfer fees, read from the ORDnet address index (ordnet-utxo). The address is checked (base58check, P2PKH only) and the locking script is derived, so a wallet can use the rows as funding inputs directly. Unconfirmed rows are marked; a first request for an address not yet watched may take a while.

{ "address": "1NdU…", "pkh": "ed40…d550", "script": "76a914…88ac",
  "utxos": [ { "txid": "…", "vout": 1, "satoshis": "250000", "scriptHex": "76a914…88ac", "height": "967010", "confirmed": true } ],
  "total_sats": "257000", "confirmed_sats": "250000", "source": "ordnet-utxo" }
GET/outpoint/{txid}/{vout}

Since 1.4.0. One output as the ORDnet node sees it: locking script, satoshis, confirmations and whether it is still unspent (gettxout). Used by wallets to fetch the active mint state script before quoting a mint.

{ "txid": "…", "vout": 0, "scriptHex": "0063036f3230010168…", "satoshis": "1", "confirmations": "12", "block_hash": "…", "unspent": true }
GET/outpoint/{txid}/{vout}/spend

Since 1.5.9. Which confirmed transaction spent this output, and in which block. Read from the indexer (ord20_spends with its block, one snapshot) and checked again with the ORDnet node: same txid, this exact output on input vin, the same block hash, at least one confirmation. confirmed_spend is true only when index and node agree. spend: null means no spend is known to the index — it is not proof that the output is unspent (plain funding spent outside ORD20 is not indexed). A reorg removes the row with its block, so check again. Never cached. Since 1.5.10 a node answer only counts when it is a complete HTTP 200 reply bound to the request id (since 1.5.11 also with "error": null); otherwise node_check is unavailable with reason protocol, transport or rpc_error.

{ "outpoint": { "txid": "…", "vout": 0, "id": "…00000000" },
  "spend": { "txid": "c7e5…9d35", "vin": 0, "block_height": "970182", "block_hash": "…", "tx_index": 431, "class": "known_valid" },
  "index_tip": { "height": "970190", "hash": "…" }, "index_confirmations": "9",
  "node_check": { "result": "match", "confirmations": "9" }, "confirmed_spend": true }
GET/address/{pkh}/utxos?token={id}

Live token/lock/mint outputs owned by a pkh (max 500). role: G = issued to the holder, R = received by transfer, M = mint state.

[ { "outpoint": "3176…01000000", "id": "aa9f…", "form": "token", "role": "G", "owner": "e8e5…", "address": "1NETFY…", "sats": "1000", "price_sats": null, "height": "967013" } ]
GET/tx/{txid}

Indexed: seen: true with block, position, roles and events. Only known to the node (mempool or not yet indexed): seen: false, node_known: true. Neither: 404 not_found.

{ "txid": "4497…", "seen": true, "block_height": "967014", "block_hash": "…", "tx_index": 903, "n_inputs": 1, "n_outputs": 1,
  "roles": ["R"], "ids": ["aa9f…"], "events": [ { "type": "melt", "amount": "193", "kind": "loss_a" }, { "type": "transfer", "amount": "807", "vout": 0 } ], "errors": [] }
POST/broadcast

Body { "rawtx": "<hex>" }. Sent to the ORDnet node with sendrawtransaction. An explicit node rejection is 400 broadcast_rejected with the node's reason; since 1.5.1 also class (mint_state_conflict, mint_state_missing, funding_spent, funding_missing or other) and per affected input {vin, outpoint, status, spent_by, mint_state}, each checked with the node. Only mint_state_conflict means someone minted first: fetch /mint-head and rebuild. A transaction the node already has is answered 200 with already_known: true, so resending the same bytes is safe. A transport or protocol failure is 502 node_unavailable: the outcome is unknown — resolve the same txid via /tx, never rebuild and resend.

{ "txid": "…", "accepted": true }
GET/tokens/{id}/transfers?limit=

Newest first. fee_from is token when the transfer paid its miner fee from the token value (a melt · loss_a event in the same transaction), else bsv.

[ { "txid": "…", "from": "1NETFY…", "from_pkh": "e8e5…", "to": "1NdU53…", "to_pkh": "ed40…", "amount": "807", "height": "967014", "fee_from": "token" } ]
GET/tokens/{id}/holders

Distribution over live token and lock outputs (top 1000 holders). Shares are percentages with two decimals, as strings.

{ "total_holders": "2", "total_units": "1807",
  "distribution": [ { "label": "Top 1", "units": "1000", "share": "55.34" }, { "label": "Top 10", … }, { "label": "Top 100", … }, { "label": "Rest", … } ],
  "rows": [ { "rank": 1, "address": "1NETFY…", "pkh": "e8e5…", "units": "1000", "share": "55.34", "outputs": "1" }, … ] }
GET/tokens/{id}/mints?limit=

Public mints (M1) and the issuer's activation (M0, amount = premint), newest first. by is public for M1, null for M0.

[ { "txid": "…", "to": "1NETFY…", "to_pkh": "e8e5…", "amount": "1000", "height": "967013", "by": "public" } ]
GET/tokens/{id}/listings?limit=

Open ORD/locked listings, cheapest per unit first.

[ { "outpoint": "…", "seller": "1…", "seller_pkh": "…", "units": "500000", "price_sats": "750000", "height": "…" } ]
GET/tokens/{id}/trades?limit=

Completed sales (sold events), newest first.

[ { "txid": "…", "units": "200000", "price_sats": "280000", "buyer": "1…", "buyer_pkh": "…", "height": "…" } ]

Errors

HTTPcodeWhen
400invalid_idan id, outpoint, pkh or txid has the wrong form (e.g. a symbol or a base58 address where a pkh is expected)
400invalid_requestbad query or body (non-JSON, non-hex rawtx, disallowed characters in q)
400broadcast_rejectedthe node explicitly rejected the transaction; message carries its reason
404not_foundunknown route, token, or transaction
503index_inconsistentsince 1.5.9, /outpoint/…/spend: the index lists more than one spend of one output; no conclusion is drawn
502node_unavailablethe node did not answer cleanly; for broadcast this means the outcome is unknown
500internalanything else; nothing about the cause is leaked