Reference

Conventions

  • Asset IDs, hashes and keys are lower-case hex; upper case is accepted and normalized.
  • Keys are compressed secp256k1 public keys (66 hex chars, 02/03 prefix).
  • Times are RFC 3339 UTC strings with microseconds, e.g. 2026-10-16T08:08:16.571212Z.
  • Errors are JSON: {"error","message","details"}, or 422 {"detail":[…]} for schema validation. Errors.
  • Send Content-Type: application/json only with a body. A GET with that header and no body gets 400 invalid_json.
  • Unknown query parameters are rejected (400).
  • Responses are gzip-compressed when you send Accept-Encoding: gzip.
  • From servers and CLIs, send X-Request-ID to correlate your logs with the registry's; it's echoed back.
  • Browsers can read every endpoint but can't send the signature headers or X-Request-ID (CORS), so signed writes must come from a server or CLI.

Errors

Registry errors are JSON with a stable error code:

{
  "error": "prev_action_hash_mismatch",
  "message": "prev_action_hash does not match latest accepted action",
  "details": {
    "asset_id": "5ac9a0f9d1b54e2c8e7f3a6b2d4c8e1f0a9b7c5d3e1f2a4b6c8d0e2f4a6b8c0d",
    "expected_prev_action_hash": "2bc27ebf045fec3c123e5ceba8ebf759911762f53693501d675a434c6de56565",
    "submitted_prev_action_hash": "0000000000000000000000000000000000000000000000000000000000000000"
  }
}

Schema validation failures use FastAPI's shape, status 422:

{
  "detail": [
    {
      "type": "string_too_short",
      "loc": [
        "path",
        "asset_id"
      ],
      "msg": "String should have at least 64 characters",
      "input": "xyz",
      "ctx": {
        "min_length": 64
      }
    }
  ]
}

On POST …/actions a body is checked against every operation type, so one bad field can produce a long detail list. Read the entries whose loc names your operation (e.g. ["body","SetCustomFieldAction","custom_key"]).

CodeStatusMeaning
validation_error400Business-rule failure, unknown query parameter, contract hash mismatch
invalid_json400Body isn't valid UTF-8 JSON, or a GET sent Content-Type: application/json without a body
non_canonical_payload400Signed body isn't canonical JSON
invalid_signature401Signature missing, malformed or not verifying
forbidden403Admin lacks the permission
asset_not_found404Unknown, deregistered, or legacy and not migrated
icon_not_found icon_proposal_not_found404No published icon / no such proposal
asset_conflict409Already registered, or ticker taken on that domain
prev_action_hash_mismatch nonce_conflict409See action chain
icon_pending_conflict icon_hash_mismatch icon_proposal_already_decided409Icon workflow conflicts
admin_conflict admin_not_found last_root_admin409 / 404Admin lifecycle
stale_timestamp no_op_action mutable_schema_version_mismatch asset_id_mismatch400Envelope problems
invalid_icon400Not a 500×500 RGBA PNG under 75,000 bytes, or bad Base64
chain_verification_failed domain_verification_failed400Issuance or domain proof doesn't match
chain_verifier_unreachable domain_verifier_unreachable503Dependency outage; retry later
rate_limited429Too many registrations or migrations from your IP (30 per 60 s by default). No Retry-After header; failed attempts count too, so wait out the window
request_body_too_large413Body over 1 MiB

Asset object

FieldDescription
asset_idLiquid asset ID
contractImmutable contract as registered. Version 0: issuer_pubkey; version 2: initial_issuer_pubkey (version 1 appears only on a few early testnet assets). Legacy extra fields are kept.
issuance_txin, issuance_prevoutWhere the asset was issued: {txid, vin} of the issuance input and {txid, vout} of the output it spends. null when the registry doesn't know them
initial_issuer_pubkeyKey at registration; never changes
initial_issuer_pubkey_sourcecontract (v2), registry_registration (legacy, not migrated), migrated_legacy_record
current_issuer_pubkeyKey that signs issuer actions now
issuer_pubkey_history[]pubkey, valid_from_audit_id, valid_until_audit_id (null = current). A key covers entries with audit_id greater than valid_from_audit_id (or equal, for the registration entry) up to and including valid_until_audit_id; a rotation entry is signed by the old key
mutablecategory_tags[], trading_venues[] (venue, url), custom (object)
adminasset_type, featured, malicious, delisted, admin_notes, last_admin_action
icon{href}: registry-relative PNG URL, or null
statusactive (deregistered assets aren't returned)
created_at, updated_atRFC 3339

Audit entry

FieldDescription
audit_idGlobal, strictly increasing
server_received_atWhen the registry accepted it (not signed)
actorsystem (registration, migration), issuer, admin
verified_pubkeyKey whose signature was verified
admin_idAdmin identity for admin actions
actionThe accepted payload (normalized). Legacy registrations and imports: {request, contract_hash}; legacy deregistrations: {asset_id, message}
action_hashsha256(canonicalJson(action)) for issuer-chain entries; null otherwise
signatureAs submitted; null for system entries

Operations: register, legacy_register, migrate_contract_metadata, replace_category_tags, replace_trading_venues, replace_custom, set_custom_field, delete_custom_field, rotate_issuer_pubkey, propose_icon, deregister, legacy_deregister, and admin operations.

Legacy entries (legacy_register, legacy_deregister, imports) have no operation field. Filter for them with ?operation=, or tell them apart by the shape of action.

For AI agents

Use the rules below as a system-prompt snippet for AI agents and code assistants.

Rules of thumb

- Base URLs: https://assets.blockstream.info (Liquid), https://assets-testnet.blockstream.info (testnet). Legacy at /, v2 under /v2. OpenAPI at /openapi.json.
- Reads are public (CORS *). Writes are signed: Bitcoin Signed Message (secp256k1) over the exact canonical-JSON body, Base64 64-byte R||S.
- Headers: Asset-Registry-Signature (issuer key), Asset-Registry-Admin-Signature (admin key). No API keys. Browsers can't send them (CORS): sign from a server or CLI.
- Formats: asset IDs, hashes and keys are lower-case hex; keys are compressed secp256k1 (66 hex, 02/03). Times are RFC 3339 UTC with microseconds.
- Canonical JSON: keys sorted recursively, no whitespace, UTF-8. Send the signed bytes unchanged.
- Issuer actions: POST /v2/assets/{id}/actions with signing_context "liquid-asset-registry-action-v1", operation, prev_action_hash (GET .../actions/latest), nonce, timestamp (RFC 3339 with microseconds).
- Mutable via issuer actions: mutable.category_tags, mutable.trading_venues, mutable.custom; also key rotation and icon proposals. The contract (name, ticker, precision, entity.domain, keys) never changes: the asset ID commits to it.
- category_tags values come from an admin-defined list (stablecoin, bond, fixed-income, tokenized today) that can change. Don't hard-code it; surface a 422 for an unknown tag.
- Retries: same signed body = idempotent_retry. 409 prev_action_hash_mismatch = refetch latest, rebuild, re-sign.
- Icons: issuers propose (500x500 PNG with alpha, < 75,000 bytes; sign only the "action" member; microsecond timestamp), admins approve. icon.href (/v2/assets/{id}/icon/{sha256}.png) never changes: cache it forever; a new icon gets a new URL. /v2/assets/{id}/icon redirects to the current one.
- Legacy (version 0) assets must be migrated (admin today) before v2 issuer actions; GET .../actions/latest returns 404 until then.
- Search: GET /v2/assets?page&page_size(<=500)&sort(<field>_asc|<field>_desc; field: asset_id, name, ticker, domain, created_at, updated_at; default asset_id_asc)&ticker&name&asset_id (prefixes)&domain (exact)&category_tag&trading_venue&asset_type&created_after&updated_after. Unknown params -> 400.
- Audit: GET /v2/assets/{id}/audit?since_audit_id&limit(<=1000)&order; page with order=asc and next_since_audit_id.
- Don't send Content-Type on GET without a body (400 invalid_json). Registration is not idempotent; rate-limited (429).
- Moderation flags (admin.delisted, admin.malicious) don't hide assets: they stay in every listing and lookup. Only v2 responses carry them (legacy shows none), so check them before displaying an asset.
- Caching: send If-Modified-Since with the last Last-Modified on any asset read (v2 or legacy); 304 = no asset changed. Last-Modified is registry-wide, removals and icons included.
- Sync: bootstrap from /v2/assets/all.json; poll sort=updated_at_desc&updated_after=<since-1s> (ties, duplicates); removals only via GET /v2/audit?operation=deregister|legacy_deregister&since_audit_id=<cursor>.
- Numbers in signed bodies: integers or strings only (non-integers must match Python float formatting).
- v2 contracts (POST /v2/assets): version must be 2, initial_issuer_pubkey required, precision 0–8, ticker 3–24; issuer_pubkey and a body-level initial_issuer_pubkey are rejected (422). Version 0 contracts go to POST /. Domain proof: HTTP file, DNS TXT, or pubkey-bound; a pubkey-bound proof also needs Asset-Registry-Signature over the normalized contract (canonical JSON, nulls removed).
- Current controlling key: current_issuer_pubkey (v2). Legacy issuer_pubkey and contract.initial_issuer_pubkey never change. issuance_txin and issuance_prevout can be null for older v2 assets.
- Key rotation: derive new_issuer_pubkey from its private key in code. The registry doesn't check it, and a wrong key locks the asset permanently (no admin reset).

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.