Legacy API (V0)

The original registry's endpoints, at the same paths and in the same shapes. Kept for compatibility.

NetworkBase URL
Liquidhttps://assets.blockstream.info
Liquid Testnethttps://assets-testnet.blockstream.info

Examples use the Liquid (mainnet) base URL. For testnet, swap in the testnet base URL.

Endpoints

MethodEndpointPath
POSTRegister (legacy)/
GETList all (legacy)/index.json
GETGet an asset (legacy)/:asset_id
GETGet all icons (legacy)/icons.json
POSTValidate a contract/contract/validate
DELETEDeregister (legacy)/:asset_id

Legacy endpoints

Register (legacy)

POST / LEGACY

Registers a version 0 contract. Same chain and domain checks as v2 (HTTP or DNS proof; no pubkey-bound proof). The asset can't use v2 issuer actions until it's migrated.

Parameters

NameIn · typeDescription
asset_id requiredbody · hex (64)The issued asset's ID.
contract requiredbody · objectVersion 0: entity.domain, issuer_pubkey, name, precision (0–8), optional ticker (3–24), version: 0. Extra fields allowed (≤ 32, ≤ 2,048 bytes each); whole contract ≤ 4,096 bytes.
domain_verification_methodbody · "http" | "dns"Default http.
🚧

Can return 400 with the full asset record even though the asset was registered. Check GET /:asset_id before retrying.

Example request

curl -X POST "https://assets.blockstream.info/" \
  -H "Content-Type: application/json" \
  -d '{"asset_id":"7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f","contract":{"entity":{"domain":"legacy-issuer.com"},"issuer_pubkey":"03566ecb682f1a49ccad62a6dbc5b4045d96d65912bbbb177b7121c27c01d91e6e","name":"Example Token","precision":0,"ticker":"EXTKN","version":0},"domain_verification_method":"http"}'
// Node.js 18+ (ES module: save as .mjs or set "type": "module")
const BASE_URL = "https://assets.blockstream.info";
const payload = {
  asset_id: "7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f",
  contract: { entity: { domain: "legacy-issuer.com" }, issuer_pubkey: "03566ecb682f1a49ccad62a6dbc5b4045d96d65912bbbb177b7121c27c01d91e6e", name: "Example Token", precision: 0, ticker: "EXTKN", version: 0 },
  domain_verification_method: "http",
};

const res = await fetch(`${BASE_URL}/`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(JSON.stringify(await res.json(), null, 2)); // full output: plain console.log cuts long strings and deep objects
# pip install requests
import requests

BASE_URL = "https://assets.blockstream.info"
payload = {
    "asset_id": "7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f",
    "contract": {"entity": {"domain": "legacy-issuer.com"}, "issuer_pubkey": "03566ecb682f1a49ccad62a6dbc5b4045d96d65912bbbb177b7121c27c01d91e6e", "name": "Example Token", "precision": 0, "ticker": "EXTKN", "version": 0},
    "domain_verification_method": "http",
}

res = requests.post(f"{BASE_URL}/", json=payload, timeout=30)
if not res.ok:
    raise RuntimeError(f"{res.status_code} {res.text}")
print(res.json())
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

func main() {
	baseURL := "https://assets.blockstream.info"
	endpoint := baseURL + "/"

	payload, err := json.Marshal(map[string]any{"asset_id": "7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f", "contract": map[string]any{"entity": map[string]any{"domain": "legacy-issuer.com"}, "issuer_pubkey": "03566ecb682f1a49ccad62a6dbc5b4045d96d65912bbbb177b7121c27c01d91e6e", "name": "Example Token", "precision": 0, "ticker": "EXTKN", "version": 0}, "domain_verification_method": "http"})
	if err != nil {
		panic(err)
	}
	req, err := http.NewRequest("POST", endpoint, bytes.NewReader(payload))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	if res.StatusCode != http.StatusCreated {
		panic(fmt.Sprintf("%d %s", res.StatusCode, body))
	}
	fmt.Println(string(body))
}
Response: 201 (local instance · application/json)
{
  "asset_id": "7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f",
  "contract": {
    "version": 0,
    "issuer_pubkey": "03566ecb682f1a49ccad62a6dbc5b4045d96d65912bbbb177b7121c27c01d91e6e",
    "name": "Example Token",
    "ticker": "EXTKN",
    "precision": 0,
    "entity": {
      "domain": "legacy-issuer.com"
    }
  },
  "version": 0,
  "issuer_pubkey": "03566ecb682f1a49ccad62a6dbc5b4045d96d65912bbbb177b7121c27c01d91e6e",
  "name": "Example Token",
  "precision": 0,
  "entity": {
    "domain": "legacy-issuer.com"
  },
  "ticker": "EXTKN"
}

List all (legacy)

GET /index.json LEGACY

Every active asset in the legacy shape, keyed by asset ID. Includes assets registered through v2.

Also available at GET / (same response).

Parameters

NameIn · typeDescription
If-Modified-Sinceheader · HTTP dateThe Last-Modified value from your previous response. Returns 304 with no body if no asset has changed since. Caching.
🚧

v2-registered assets appear with version: 2, their key in contract.initial_issuer_pubkey, and no top-level issuer_pubkey. See Migrating from legacy.

Example request

curl "https://assets.blockstream.info/index.json"
// Node.js 18+ (ES module: save as .mjs or set "type": "module")
const BASE_URL = "https://assets.blockstream.info";

const res = await fetch(`${BASE_URL}/index.json`);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(JSON.stringify(await res.json(), null, 2)); // full output: plain console.log cuts long strings and deep objects
# pip install requests
import requests

BASE_URL = "https://assets.blockstream.info"

res = requests.get(f"{BASE_URL}/index.json", timeout=30)
if not res.ok:
    raise RuntimeError(f"{res.status_code} {res.text}")
print(res.json())
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	baseURL := "https://assets.blockstream.info"
	endpoint := baseURL + "/index.json"

	res, err := http.Get(endpoint)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	if res.StatusCode != http.StatusOK {
		panic(fmt.Sprintf("%d %s", res.StatusCode, body))
	}
	fmt.Println(string(body))
}
Response: 200 (production · application/json · 14,230 entries, first shown)
{
  "aa909f1b77451e409fe95fe1d3638ad017ab3325c6d4f00301af6d582d0f2034": {
    "name": "BMN Series 2",
    "entity": {
      "domain": "blockstream-mining2.stokr.io"
    },
    "ticker": "BMN2",
    "version": 0,
    "asset_id": "aa909f1b77451e409fe95fe1d3638ad017ab3325c6d4f00301af6d582d0f2034",
    "contract": {
      "name": "BMN Series 2",
      "entity": {
        "domain": "blockstream-mining2.stokr.io"
      },
      "ticker": "BMN2",
      "version": 0,
      "precision": 4,
      "issuer_pubkey": "0206eb0dd95be6ace6d83e5e865815d73bed9e553a1a829fe8c2de74189ee2289b"
    },
    "precision": 4,
    "collection": null,
    "issuance_txin": {
      "txid": "9bd4592c4238146e3b116f0b307100e38d58a26c62465e53c0ba7b8511ce7c88",
      "vin": 0
    },
    "issuer_pubkey": "0206eb0dd95be6ace6d83e5e865815d73bed9e553a1a829fe8c2de74189ee2289b",
    "issuance_prevout": {
      "txid": "b9317d00e44ab9f4ce435677b9b3f915e09d12b2be648da6bd12cd790d12658d",
      "vout": 3
    }
  }
}

Get an asset (legacy)

GET /:asset_id LEGACY

One active asset in the legacy shape. Assets imported from the original registry keep their original fields, such as issuance_txin and issuance_prevout.

Parameters

NameIn · typeDescription
asset_id requiredpath · hex (64)Liquid asset ID. Upper-case hex is accepted and normalized.
If-Modified-Sinceheader · HTTP dateThe Last-Modified value from your previous response. Returns 304 with no body if no asset has changed since. Caching.

Example request

curl "https://assets.blockstream.info/aa909f1b77451e409fe95fe1d3638ad017ab3325c6d4f00301af6d582d0f2034"
// Node.js 18+ (ES module: save as .mjs or set "type": "module")
const BASE_URL = "https://assets.blockstream.info";
const ASSET_ID = "aa909f1b77451e409fe95fe1d3638ad017ab3325c6d4f00301af6d582d0f2034";

const res = await fetch(`${BASE_URL}/${ASSET_ID}`);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(JSON.stringify(await res.json(), null, 2)); // full output: plain console.log cuts long strings and deep objects
# pip install requests
import requests

BASE_URL = "https://assets.blockstream.info"
ASSET_ID = "aa909f1b77451e409fe95fe1d3638ad017ab3325c6d4f00301af6d582d0f2034"

res = requests.get(f"{BASE_URL}/{ASSET_ID}", timeout=30)
if not res.ok:
    raise RuntimeError(f"{res.status_code} {res.text}")
print(res.json())
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	baseURL := "https://assets.blockstream.info"
	assetID := "aa909f1b77451e409fe95fe1d3638ad017ab3325c6d4f00301af6d582d0f2034"
	endpoint := baseURL + "/" + assetID

	res, err := http.Get(endpoint)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	if res.StatusCode != http.StatusOK {
		panic(fmt.Sprintf("%d %s", res.StatusCode, body))
	}
	fmt.Println(string(body))
}
Response: 200 (production · application/json)
{
  "name": "BMN Series 2",
  "entity": {
    "domain": "blockstream-mining2.stokr.io"
  },
  "ticker": "BMN2",
  "version": 0,
  "asset_id": "aa909f1b77451e409fe95fe1d3638ad017ab3325c6d4f00301af6d582d0f2034",
  "contract": {
    "name": "BMN Series 2",
    "entity": {
      "domain": "blockstream-mining2.stokr.io"
    },
    "ticker": "BMN2",
    "version": 0,
    "precision": 4,
    "issuer_pubkey": "0206eb0dd95be6ace6d83e5e865815d73bed9e553a1a829fe8c2de74189ee2289b"
  },
  "precision": 4,
  "collection": null,
  "issuance_txin": {
    "txid": "9bd4592c4238146e3b116f0b307100e38d58a26c62465e53c0ba7b8511ce7c88",
    "vin": 0
  },
  "issuer_pubkey": "0206eb0dd95be6ace6d83e5e865815d73bed9e553a1a829fe8c2de74189ee2289b",
  "issuance_prevout": {
    "txid": "b9317d00e44ab9f4ce435677b9b3f915e09d12b2be648da6bd12cd790d12658d",
    "vout": 3
  }
}
A v2-registered asset in the legacy shape (testnet): 200 (production · application/json)
{
  "asset_id": "a20caa0e4bff1a884d46e8b65571294569e041e8eee72104afc2269582d37853",
  "contract": {
    "entity": {
      "domain": "liquidtestnet.com"
    },
    "name": "Blockstream Bond 2",
    "precision": 2,
    "version": 2,
    "initial_issuer_pubkey": "02604044b90e208a4baf09ca37aa22b3775c81d80c717e81f642eb7bce922c5d7c",
    "ticker": "B-Bond2"
  },
  "version": 2,
  "name": "Blockstream Bond 2",
  "precision": 2,
  "entity": {
    "domain": "liquidtestnet.com"
  },
  "ticker": "B-Bond2"
}

Get all icons (legacy)

GET /icons.json LEGACY

Every approved icon as Base64 PNG, keyed by asset ID, in one response. Includes icons approved through the v2 workflow and the L-BTC icon.

Parameters

NameIn · typeDescription
If-Modified-Sinceheader · HTTP dateThe Last-Modified value from your previous response. Returns 304 with no body if no asset has changed since. Caching.
📘

Sent with Cache-Control: no-cache and the registry-wide Last-Modified, so any asset change, not only an icon change, makes it new. Send If-Modified-Since to get a 304 instead of re-downloading the whole map.

👍

Prefer each asset's icon.href: immutable, cacheable, and fetched only when you need it.

Example request

curl "https://assets.blockstream.info/icons.json"
// Node.js 18+ (ES module: save as .mjs or set "type": "module")
const BASE_URL = "https://assets.blockstream.info";

const res = await fetch(`${BASE_URL}/icons.json`);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(JSON.stringify(await res.json(), null, 2)); // full output: plain console.log cuts long strings and deep objects
# pip install requests
import requests

BASE_URL = "https://assets.blockstream.info"

res = requests.get(f"{BASE_URL}/icons.json", timeout=30)
if not res.ok:
    raise RuntimeError(f"{res.status_code} {res.text}")
print(res.json())
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	baseURL := "https://assets.blockstream.info"
	endpoint := baseURL + "/icons.json"

	res, err := http.Get(endpoint)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	if res.StatusCode != http.StatusOK {
		panic(fmt.Sprintf("%d %s", res.StatusCode, body))
	}
	fmt.Println(string(body))
}
Response: 200 (production · application/json · 1,059 entries, first shown)
{
  "aa909f1b77451e409fe95fe1d3638ad017ab3325c6d4f00301af6d582d0f2034": "iVBORw0KGgoAAAANSUhEUgAAAfQAAAH0CAYAAADL1t+KAAAA…"
}

Validate a contract

POST /contract/validate LEGACY

Checks that a version 0 contract hashes to the given contract hash. Returns valid as plain text, or 409 with the expected hash. Writes nothing. Version 1 and 2 contracts are rejected (422); compute their hash yourself (Register an asset).

Parameters

NameIn · typeDescription
contract requiredbody · objectVersion 0 contract.
contract_hash requiredbody · hex (64)Hash you committed at issuance.

Example request

curl -X POST "https://assets.blockstream.info/contract/validate" \
  -H "Content-Type: application/json" \
  -d '{"contract":{"name":"BMN Series 2","entity":{"domain":"blockstream-mining2.stokr.io"},"ticker":"BMN2","version":0,"precision":4,"issuer_pubkey":"0206eb0dd95be6ace6d83e5e865815d73bed9e553a1a829fe8c2de74189ee2289b"},"contract_hash":"998614e8941c5cc4fd674e1d9b29e53468ced64290ec5742273659ef103cc461"}'
// Node.js 18+ (ES module: save as .mjs or set "type": "module")
const BASE_URL = "https://assets.blockstream.info";
const payload = {
  contract: { name: "BMN Series 2", entity: { domain: "blockstream-mining2.stokr.io" }, ticker: "BMN2", version: 0, precision: 4, issuer_pubkey: "0206eb0dd95be6ace6d83e5e865815d73bed9e553a1a829fe8c2de74189ee2289b" },
  contract_hash: "998614e8941c5cc4fd674e1d9b29e53468ced64290ec5742273659ef103cc461",
};

const res = await fetch(`${BASE_URL}/contract/validate`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.text());
# pip install requests
import requests

BASE_URL = "https://assets.blockstream.info"
payload = {
    "contract": {"name": "BMN Series 2", "entity": {"domain": "blockstream-mining2.stokr.io"}, "ticker": "BMN2", "version": 0, "precision": 4, "issuer_pubkey": "0206eb0dd95be6ace6d83e5e865815d73bed9e553a1a829fe8c2de74189ee2289b"},
    "contract_hash": "998614e8941c5cc4fd674e1d9b29e53468ced64290ec5742273659ef103cc461",
}

res = requests.post(f"{BASE_URL}/contract/validate", json=payload, timeout=30)
if not res.ok:
    raise RuntimeError(f"{res.status_code} {res.text}")
print(res.text)
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

func main() {
	baseURL := "https://assets.blockstream.info"
	endpoint := baseURL + "/contract/validate"

	payload, err := json.Marshal(map[string]any{"contract": map[string]any{"name": "BMN Series 2", "entity": map[string]any{"domain": "blockstream-mining2.stokr.io"}, "ticker": "BMN2", "version": 0, "precision": 4, "issuer_pubkey": "0206eb0dd95be6ace6d83e5e865815d73bed9e553a1a829fe8c2de74189ee2289b"}, "contract_hash": "998614e8941c5cc4fd674e1d9b29e53468ced64290ec5742273659ef103cc461"})
	if err != nil {
		panic(err)
	}
	req, err := http.NewRequest("POST", endpoint, bytes.NewReader(payload))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	if res.StatusCode != http.StatusOK {
		panic(fmt.Sprintf("%d %s", res.StatusCode, body))
	}
	fmt.Println(string(body))
}
Response: 200 (production · text/plain)
valid
Error: hash mismatch: 409 (production · application/json)
{
  "error": "validation_error",
  "message": "contract hash mismatch, expected 998614e8941c5cc4fd674e1d9b29e53468ced64290ec5742273659ef103cc461",
  "details": {
    "expected_contract_hash": "998614e8941c5cc4fd674e1d9b29e53468ced64290ec5742273659ef103cc461"
  }
}

Deregister (legacy)

DELETE /:asset_id SIGNED LEGACY

Deregisters an asset with a signed message. Works for any active asset, including v2-registered ones.

Parameters

NameIn · typeDescription
asset_id requiredpath · hex (64)Liquid asset ID. Upper-case hex is accepted and normalized.
signature requiredbody · Base64Bitcoin Signed Message signature of remove <asset_id> from registry with the asset's current issuer key.

Example request

# Sign "remove <asset_id> from registry" with the current issuer key (see Signing requests)
SIG='0Twie/nHa6rwIsj7Ma0OaTO8lSGjR2ifU99tbA85ucxdKddWbfsn9GYWEmPc61fZnDTYBLjMOmlAqcLGyJ5ouw=='

curl -X DELETE "https://assets.blockstream.info/7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f" \
  -H "Content-Type: application/json" \
  -d "{\"signature\":\"$SIG\"}"
// Node.js 18+ (ES module: save as .mjs or set "type": "module")
import { createECDH, createHash, createPrivateKey, sign } from "node:crypto";

const BASE_URL = "https://assets.blockstream.info";
const ASSET_ID = "7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f";
const PRIVATE_KEY = process.env.ISSUER_PRIVATE_KEY; // 32-byte hex: the asset's current issuer key

const signature = signMessage(`remove ${ASSET_ID} from registry`, PRIVATE_KEY);

const res = await fetch(`${BASE_URL}/${ASSET_ID}`, {
  method: "DELETE",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ signature }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.text());

// --- signing helpers (see Signing requests) ---
// Canonical JSON: keys sorted recursively, no whitespace. Use integers or strings for numbers
// (non-integer numbers are formatted differently by JavaScript and the registry).
function canonicalJson(v) {
  if (Array.isArray(v)) return `[${v.map(canonicalJson).join(",")}]`;
  if (v && typeof v === "object") return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(v[k])}`).join(",")}}`;
  return JSON.stringify(v);
}

// Bitcoin Signed Message signature (Base64, 64-byte R||S) over the UTF-8 message.
function signMessage(message, privateKeyHex) {
  const varint = (n) => (n < 0xfd ? Buffer.from([n]) : Buffer.from([0xfd, n & 0xff, n >> 8]));
  const msg = Buffer.from(message, "utf8");
  const magic = Buffer.from("Bitcoin Signed Message:\n");
  const firstHash = createHash("sha256").update(Buffer.concat([varint(magic.length), magic, varint(msg.length), msg])).digest();
  const d = Buffer.from(privateKeyHex, "hex");
  const ecdh = createECDH("secp256k1");
  ecdh.setPrivateKey(d);
  const pub = ecdh.getPublicKey();
  const key = createPrivateKey({ format: "jwk", key: { kty: "EC", crv: "secp256k1", d: d.toString("base64url"),
    x: pub.subarray(1, 33).toString("base64url"), y: pub.subarray(33).toString("base64url") } });
  const sig = sign("sha256", firstHash, { key, dsaEncoding: "ieee-p1363" }); // ECDSA-SHA256 adds the second hash
  const n = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
  const s = BigInt("0x" + sig.subarray(32).toString("hex"));
  if (s > n / 2n) sig.write((n - s).toString(16).padStart(64, "0"), 32, "hex"); // low-S form
  return sig.toString("base64");
}

// Compressed public key (66 hex) for a private key.
function publicKeyOf(privateKeyHex) {
  const ecdh = createECDH("secp256k1");
  ecdh.setPrivateKey(Buffer.from(privateKeyHex, "hex"));
  return ecdh.getPublicKey("hex", "compressed");
}
# pip install requests cryptography==50.0.2
import base64
import hashlib
import json
import os

import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature

BASE_URL = "https://assets.blockstream.info"
ASSET_ID = "7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f"
PRIVATE_KEY = os.environ["ISSUER_PRIVATE_KEY"]  # 32-byte hex: the asset's current issuer key

def canonical_json(value):
    """Canonical JSON: keys sorted recursively, no whitespace. Prefer integers or strings for numbers."""
    return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)

def sign_message(message, private_key_hex):
    """Bitcoin Signed Message signature (Base64, 64-byte R||S) over the UTF-8 message."""
    msg = message.encode("utf-8")
    varint = lambda n: bytes([n]) if n < 0xFD else b"\xfd" + n.to_bytes(2, "little")
    payload = varint(24) + b"Bitcoin Signed Message:\n" + varint(len(msg)) + msg
    key = ec.derive_private_key(int(private_key_hex, 16), ec.SECP256K1())
    first_hash = hashlib.sha256(payload).digest()  # ECDSA-SHA256 adds the second hash
    r, s = decode_dss_signature(key.sign(first_hash, ec.ECDSA(hashes.SHA256())))
    n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
    s = min(s, n - s)  # low-S form
    return base64.b64encode(r.to_bytes(32, "big") + s.to_bytes(32, "big")).decode()

def public_key_of(private_key_hex):
    """Compressed public key (66 hex) for a private key."""
    key = ec.derive_private_key(int(private_key_hex, 16), ec.SECP256K1())
    return key.public_key().public_bytes(serialization.Encoding.X962, serialization.PublicFormat.CompressedPoint).hex()

signature = sign_message(f"remove {ASSET_ID} from registry", PRIVATE_KEY)
res = requests.delete(f"{BASE_URL}/{ASSET_ID}", json={"signature": signature}, timeout=30)
if not res.ok:
    raise RuntimeError(f"{res.status_code} {res.text}")
print(res.text)
// go mod init example && go get github.com/decred/dcrd/dcrec/secp256k1/[email protected]
package main

import (
	"bytes"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"

	"github.com/decred/dcrd/dcrec/secp256k1/v4"
	"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
)

func main() {
	baseURL := "https://assets.blockstream.info"
	assetID := "7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f"
	endpoint := baseURL + "/" + assetID
	privateKey := os.Getenv("ISSUER_PRIVATE_KEY") // 32-byte hex: the asset's current issuer key

	sig := signMessage([]byte("remove "+assetID+" from registry"), privateKey)
	payload, err := json.Marshal(map[string]string{"signature": sig})
	if err != nil {
		panic(err)
	}
	req, err := http.NewRequest("DELETE", endpoint, bytes.NewReader(payload))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	if res.StatusCode != http.StatusOK {
		panic(fmt.Sprintf("%d %s", res.StatusCode, body))
	}
	fmt.Println(string(body))
}

// canonicalJSON: keys sorted (Go sorts map keys), no whitespace, no HTML escaping.
func canonicalJSON(v any) []byte {
	var buf bytes.Buffer
	enc := json.NewEncoder(&buf)
	enc.SetEscapeHTML(false)
	if err := enc.Encode(v); err != nil {
		panic(err)
	}
	return bytes.TrimRight(buf.Bytes(), "\n")
}

// signMessage returns a Bitcoin Signed Message signature (Base64, 64-byte R||S).
func signMessage(message []byte, privateKeyHex string) string {
	key, err := hex.DecodeString(privateKeyHex)
	if err != nil {
		panic(err)
	}
	var payload bytes.Buffer
	writeVarint(&payload, 24)
	payload.WriteString("Bitcoin Signed Message:\n")
	writeVarint(&payload, len(message))
	payload.Write(message)
	first := sha256.Sum256(payload.Bytes())
	digest := sha256.Sum256(first[:])
	sig := ecdsa.SignCompact(secp256k1.PrivKeyFromBytes(key), digest[:], true) // recovery byte + R||S, low-S
	return base64.StdEncoding.EncodeToString(sig[1:])
}

func writeVarint(b *bytes.Buffer, n int) {
	if n < 0xfd {
		b.WriteByte(byte(n))
		return
	}
	b.WriteByte(0xfd)
	b.WriteByte(byte(n))
	b.WriteByte(byte(n >> 8))
}

// publicKeyOf returns the compressed public key (66 hex) for a private key.
func publicKeyOf(privateKeyHex string) string {
	key, err := hex.DecodeString(privateKeyHex)
	if err != nil {
		panic(err)
	}
	return hex.EncodeToString(secp256k1.PrivKeyFromBytes(key).PubKey().SerializeCompressed())
}
Response: 200 (local instance · text/plain)
Asset deleted

The Liquid Network is a production Bitcoin sidechain built with Elements, supporting confidential transactions, asset issuance and programmable smart contracts.

© 2026 Liquid Network
All rights reserved.

Feedback and Requests

We'd be happy to hear your suggestions on how to improve this site.

Simplicity Forum

Discuss SimplicityHL, tooling and applications on Liquid at community.simplicity-lang.org.

Liquid Dev Community

Community Telegram group where most of the Liquid developers hang out. Go to t.me/liquid_devel to join.