Liquid Securities API Tutorial

This tutorial outlines the most common process of creating and managing Issued Assets as Security Tokens using the Liquid Securities API.

We’ll walk through the code in the complete python example and describe the process in more detail when needed. You can also reference the API Specification for a full list and description of all the Liquid Securities API endpoints.

Definitions of the terms used in this page, as well as an overview of how Liquid Securities uses the Liquid network to issue and manage security tokens, can be found in the Liquid Asset Registry.

A brief overview of Liquid Securities can also be found on the Blockstream website.

Managing Security Tokens with Liquid Securities

There are two ways you can manage a token in Liquid Securities. Both restrict transfers so that they can only occur between investors when the transacting investors are using Green Liquid Securities account wallets. Additional restrictions can then be applied by using Liquid Securities to manage and apply Investor and asset restrictions.

When an asset relies on Green wallet alone to restrict transfers it is called a Tracked Assets and when it uses further restrictions, defined by the issuer within Liquid Securities, it is referred to as an Investor Restricted Asset.

The simplest case of issuing an Investor Restricted Asset, and managing it as a security token using Liquid Securities, is outlined below.

  • Issue a Liquid Asset representing a Security.

  • Create Investor/Asset Categories.

  • Associate categories with the asset to create asset requirements.

  • Create Investors.

  • Associate categories with investors to define investor eligibility.

  • Assign amounts of the asset to eligible investors.

  • Distribute the assigned amounts to the investor’s wallets.

This process is followed by the tutorial code. A subset of the API can be used to manage Tracked Assets.

Frequently Used Terms

The most commonly used API endpoints are those which relate to the Asset. An asset in Liquid Securities represents the actual Security, while amounts of the asset represent tokens for the Security. The ‘Asset’ terminology is inherited from the Liquid Network’s Issued Assets. It should be noted that there is another type of asset used by Liquid called the Reissuance Token. Reissuance Tokens allow the issuer to create more of the asset, and can only be created at the point of initial asset issuance. Reissuance Tokens and Security Tokens should not be confused with one another.

Setting up a Treasury Wallet

Issuers should first set up Elements Core, as their Liquid Securities Treasury Wallet. The treasury wallet is used to receive issued assets, reissuance tokens, and carry out actions such as distributions and reissuances. To install and configure Elements Core, follow the node setup guide.

Accessing Liquid Securities API

The live Liquid Securities API endpoints are available through paths starting https://securities.blockstream.com/api/.

A test version of the API is available through paths starting https://securities-beta.blockstream.com/api/. The test version connects to the live Liquid Network, enabling investor transfer testing with Green Liquid Securities wallets.

To use the Liquid Securities API, a username and password must be provided so that the server can authenticate requests. You can request account access by emailing liquid-securities-support@blockstream.com.

API Authentication

Before any API calls can be made, you need to obtain an Authorization Token using the Liquid Securities account details you were provided with.

The authorization token can then be set in the header of subsequent requests. An example of how to obtain the Authorization Token is shown below. Remember that the code shown here is taken from the complete python code example.

def get_auth_token_header(username, password):
    # api/user/obtain_token
    url = f'{API_URL}/user/obtain_token'
    headers = {'content-type': 'application/json'}
    payload = {'username': username, 'password': password}
    response = requests.post(url, data=json.dumps(payload), headers=headers)
    assert response.status_code == 200
    json_data = json.loads(response.text)
    token = json_data['token']
    return {'content-type': 'application/json', 'Authorization': f'token {token}'}

Once we have an Authorization Token we can begin the process of managing investors, issuing assets as Security Tokens, distribution and reporting on ownership etc.

Investors

Liquid Securities allows for restricting asset transfers through the use of investor records. Investor records that have a Green Liquid Securities Account ID (GAID) saved against them are able to receive the Security Token, although other restrictions might apply through Investor Categories, explained later.

Note

To find your Liquid Securities Green Account ID within the Green mobile app, go into the Liquid Securities account and click ‘Receive’.

Adding, Viewing, Updating and Listing Investors

Before you actually create investor records you may want to validate the investor data you have collected. The validation of the Green Liquid Securities Account ID (GAID) can be done using the api/gaids/{gaid}/validate as shown below. This can be useful if you want to validate investor GAIDs before trying to create investors.

gaid = 'INVALIDZZ6SqZZvEpAeVz9QmfHqhh'
url = f'{API_URL}/gaids/{gaid}/validate'
response = requests.get(url, headers=headers)
assert response.status_code == 200
gaid_validation = json.loads(response.text)
gaid_is_valid = gaid_validation['is_valid']
print(f'\nValidating GAID \'{gaid}\'')
print(f'GAID is Valid: {gaid_is_valid}')

Although validating the GAID before investor record creation is useful it it not necessary as investors/add also validates the GAID before allowing the investor record to be created. This api also accepts a list of investors. If a list is provided and one or more of the investors fails validation, none of the investors will be created and a list of errors will be returned. You must add a valid GAID before running the example add below.

Creating the investor record:

url = f'{API_URL}/investors/add'
investor_name = f'Investor {rand_string()}'
payload = {'is_company': False,
    'name': investor_name,
    'GAID': ''
}
response = requests.post(url, data=json.dumps(payload), headers=headers)
assert response.status_code == 201
investor = json.loads(response.text)
investor_id = int(investor['id'])
print('\nNew investor (individual):')
print('--------------------------')
print(json.dumps(investor, indent=4))

The use of rand_string in the full example code is just to ensure that multiple runs of the code block do not fail with duplicate investor errors, it is not intended for use when importing real investor data.

Note that the investors/add API also accepts a list of investors. If a list is provided and one or more of the investors fails validation, none of the investors will be created and a list of errors will be returned.

Issuers can also retrieve a list of investors and the details of a specific investor. You can also delete investors (not shown).

# Investors details
# api/investors/{investor_id}
url = f'{API_URL}/investors/{investor_id}'
response = requests.get(url, headers=headers)
assert response.status_code == 200
investor_details = json.loads(response.text)
print('\nInvestor details:')
print('-----------------')
print(json.dumps(investor_details, indent=4))

# Investors update name
# api/investors/{investor_id}/edit
url = f'{API_URL}/investors/{investor_id}/edit'
payload = {'name': f'{investor_name} updated'}
response = requests.put(url, data=json.dumps(payload), headers=headers)
assert response.status_code == 200
investor_details = json.loads(response.text)
print('\nInvestor details:')
print('-----------------')
print(json.dumps(investor_details, indent=4))

# Investors list
# api/investors
url = f'{API_URL}/investors'
response = requests.get(url, headers=headers)
assert response.status_code == 200
investors = json.loads(response.text)
print('\nInvestors list:')
print('---------------')
for investor in investors:
    print(json.dumps(investor, indent=4))

Categories

Liquid Securities is able to restrict the transfer of assets between sender and receiver using user-defined Categories. Categories can be viewed as asset requirements that investors need to be associated with in order to qualify for assignment and distributions of the asset.

Investors can be assigned to one or more categories. By associating categories with an asset you can define the requirements for an investor to receive the asset. Categories can be used to classify investors in various ways, such as by jurisdiction, accreditation status, or any other arbitrary classification that can be used to satisfy the requirements of the asset.

Issuers then create categories required for the issued asset. The simplest method of doing this is to define an individual whitelist for each asset, however multiple assets can share the same whitelist.

Adding, Viewing, Updating and Listing Categories

Creating a Category:

# Category add
# api/categories/add
url = f'{API_URL}/categories/add'
category_name = f'Category {rand_string()}'
payload = {'name': category_name,
    'description':'Category for example'}
response = requests.post(url, data=json.dumps(payload), headers=headers)
assert response.status_code == 201
category = json.loads(response.text)
category_id = int(category['id'])
print('\nNew category:')
print('-------------')
print(json.dumps(category, indent=4))

Issuers can also retrieve a list of categories and the category details for a specific category. You can also edit a category and delete a category (not shown).

# Category details
# api/categories/{category_id}
url = f'{API_URL}/categories/{category_id}'
response = requests.get(url, headers=headers)
assert response.status_code == 200
category_details = json.loads(response.text)
print('\nCategory details:')
print('-----------------')
print(json.dumps(category_details, indent=4))

# Category update
# api/categories/{category_id}/edit
url = f'{API_URL}/categories/{category_id}/edit'
payload = {'name': f'{category_name} updated', 'description':'Category example updated'}
response = requests.put(url=url, data=json.dumps(payload), headers=headers)
assert response.status_code == 200
category_details = json.loads(response.text)
print('\nCategory details:')
print('-----------------')
print(json.dumps(category_details, indent=4))

# Category list
# api/categories
url = f'{API_URL}/categories'
response = requests.get(url, headers=headers)
assert response.status_code == 200
categories = json.loads(response.text)
print('\nCategories list:')
print('----------------')
for category in categories:
    print(json.dumps(category, indent=4))

Associating Investors with Categories

The issuer can then associate investors with the categories. If multiple categories have been associated with the asset, investor membership to all categories is required. Investors can be added to the category and removed from the category at any point.

In order to receive security tokens, an investor will need to provide a Green ‘Liquid Securities Account ID’ (GAID). This can be provided during investor creation or by editing the investor.

Note

Blockstream Green’s Authorized Asset Wallet holds assets in a multi-signature scheme that requires the Green server to act as co-signer when authorizing transfers. Investors who do not have wallets set up can still be added to Liquid Securities, but cannot receive any distributions or transfers of assets until their wallet is registered. Each wallet is able to use multiple addresses to avoid privacy leaks about other holdings.

# Investor category - add investor to category
# api/categories/{categoryId}/investors/add
url = f'{API_URL}/categories/{category_id}/investors/add'
payload = {[investor_id,]}
response = requests.post(url, data=json.dumps(payload), headers=headers)
assert response.status_code == 200

Note that the investor list and investor details views both show the categories associated with the investor/s.

# Investor details - check category was added
# api/investors/{investor_id}
url = f'{API_URL}/investors/{investor_id}'
response = requests.get(url, headers=headers)
assert response.status_code == 200
investor_details = json.loads(response.text)
print('\nInvestor details:')
print('-----------------')
print(json.dumps(investor_details, indent=4))

Categories also need to be associated with Assets before amounts of the asset can be assigned to any investors, this is covered within the next section.

Issuing Assets as Security Tokens

Security Tokens will be referred to as assets within this tutorial, as the term ‘token’ also refers to a special type of asset, the ‘reissuance token’, that permits the reissuance of an asset on the Liquid network.

Issuers choose the initial asset supply, the destination address is where the asset and any reissuance tokens will be received, and use the Liquid Securities API to issue the asset. When the asset is issued, Liquid Securities will issue it directly to the treasury addresses provided.

Issuing an Asset

If the asset contains registration data (ticker, domain, pubkey) later updates will not be possible as they are committed to the issuance transaction. For more information on the use of these three fields, please see the Liquid Asset Registry section.

If is_reissuable is true then reissuance_amount and reissuance_address must also be provided, and reissuance_address must be different from destination_address.

You will need to amend the following values from the example values given below:

name: The name of the Asset as it will appear in Liquid Securities. Length must be 5 to 255 ascii characters.

amount: The amount of the asset to issue. Integer, minimum: 1, maximum: 2100000000000000.

is_confidential: If true, the issuance amount will not be readable on the Liquid blockchain. For most issuances it is expected that this will be False.

destination_address: An address generated by your Liquid node that will receive the issued asset.

domain: The domain that will be used to verify the asset. Must be a valid domain name format, for example: example.com or sub.example.com. Do not include the http/s or www prefixes.

ticker: The ticker you would like to assign to the asset. Length must be 3 to 5 characters. Valid characters are a to z, A to Z, ‘.’ and ‘-‘.

pubkey: Pubkey for the Liquid Asset Registry, must be a compressed pubkey in hex. You can obtian the pubkey from an Elements address using the Elements getaddressinfo rpc command.

is_reissuable: If true, the asset will be created as reissuable.

reissuance_address: The address that will receive the reissuance token if is_reissuable is set to True.

reissuance_amount: The amount of reissuance tokens to create if is_reissuable is set to True.

destination_address: An address generated by your Liquid node that will receive the issued asset for the example.

reissuance_address: An address generated by your Liquid node that will receive the reissuance token for the example.

asset_destination_address = ''
reissuance_token_destination_address = ''

url = f'{API_URL}/assets/issue'
payload = {'name': 'An example asset with registration data',
            'amount': 17000000,
            'is_confidential': True,
            'destination_address': asset_destination_address,
            'domain': 'yourdomainforregistrationproof.com',
            'ticker': 'LSEXA',
            'pubkey': '02' * 33,
            'is_reissuable': True,
            'reissuance_address': reissuance_token_destination_address,
            'reissuance_amount': 1,}
response = requests.post(url=url, data=json.dumps(payload), headers=headers)
assert response.status_code == 201
asset = json.loads(response.text)
asset_uuid = asset['asset_uuid']
print('\nNewly issued (registration enabled) asset:')
print('------------------------------------------')
print(json.dumps(asset, indent=4))

# Actions such as Reissuance, Authorized asset registration, Assignment
# require more than one confirmation of the issuance transaction:
time.sleep(3*60)

It is worth noting that there is an assets/{asset_uuid}/delete API which does not destroy the asset on the Liquid network, instead it marks it as deleted within Liquid Securities, so that it does not show up in any subsequent API calls. Some details of the asset cannot be edited as they are linked to either Liquid blockchain data or the Liquid Asset Registry.

Note

Liquid Securities never holds the asset on the issuer’s behalf. The issuer must take precautions to safely store the private keys that control the treasury balance.

Other useful Asset related API to note:

# List issued assets
# api/assets
url = f'{API_URL}/assets'
response = requests.get(url, headers=headers)
assert response.status_code == 200
assets = json.loads(response.text)
print('\nIssued assets:')
print('--------------')
for asset in assets:
    print(json.dumps(asset, indent=4))

# Asset details
# api/assets/{asset_uuid}
url = f'{API_URL}/assets/{asset_uuid}'
response = requests.get(url, headers=headers)
assert response.status_code == 200
asset = json.loads(response.text)
print(f'\nAsset details:')
print('--------------')
print(json.dumps(asset, indent=4))

Liquid Asset Registry and Blockstream Green

During issuance, the issuer can provide information that allows the asset to later be added to the Liquid Asset Registry. By doing this, it can be made available to services that use the data, such as Blockstream Green. To enable registration, the issuer chooses a ticker, the domain name to associate and validate ownership of the asset registration, and a public key that can be used to update the registration data. After issuance, and after the domain proof has been set up, the assets/{asset_uuid}/register API can then be called. This is not shown in the example code as the process involves steps outside the API to be carried out first, details of which can be found in the Liquid Asset Registry section.

The asset must also be registered with Blockstream Green, which allows the Green server to authorize and co-sign valid transfers. This is important during Investor creation, assignment, distribution, and for any subsequent transfers:

# Register the asset as an Authorized Asset
# api/assets/{asset_uuid}/register-authorized
url = f'{API_URL}/assets/{asset_uuid}/register-authorized'
response = requests.get(url, headers=headers)
assert response.status_code == 200
register_authorized = json.loads(response.text)
print('\nRegister authorized summary:')
print('----------------------------')
print(json.dumps(register_authorized, indent=4))

Associating Assets with Categories

Issuers then add categories to the asset, so that they become requirements that investors will need to satisfy in order to be valid for assignments of the asset. Categories can be added to the asset or removed from the asset at any point.

# Add the Category to the Asset as a requirement
# api/assets/{assetUuid}/categories/add
url = f'{API_URL}/assets/{asset_uuid}/categories/add'
payload = {[category_id,]}
response = requests.post(url=url, data=json.dumps(payload), headers=headers)
assert response.status_code == 200
asset = json.loads(response.text)
requirements = asset['requirements']
print('\nAsset Requirements (Investor Categories ids):')
print('---------------------------------------------')
print(json.dumps(requirements, indent=4))

Reissuing an Asset

To issue more of an asset, the reissue request endpoint is called. This returns reissuance data that must be run against the treasury’s node using the liquid-securities-confirm.py Python client, which is provided as part of account creation. The client will perform the reissuance, wait for sufficient blocks, and then confirm the reissuance transaction by sending transaction information back to Liquid Securities. This ensures that asset ownership is tracked properly.

# Reissue some of the asset.
# Request the data needed to carry out a reissuance.
# api/assets/{assetUuid}/reissue-request
# Returns json data that should be saved to file. The path to the file should be passed as
# an argument to a Python client that carries out the reissuance itself. The client will
# be supplied to users of the api and will handle the reissuance transaction and post back
# the resulting transaction data to the /assets/{assetUuid}/reissue-confirm endpoint after
# it has sufficient confirmations.
# NOTE: Before the reissuance can be made, the transaction in which the asset was issued
# needs to have had at least 2 confirmations on the Liquid blockchain or the reissuance
# will fail.
url = f'{API_URL}/assets/{asset_uuid}/reissue-request'
payload = {'amount_to_reissue':1}
response = requests.post(url=url, data=json.dumps(payload), headers=headers)
assert response.status_code == 200
reissuance_data = json.loads(response.text)
# The json data in reissuance_data would then be saved to a file for use in the Python client
# that carries out the reissuance itself.
print(json.dumps(reissuance_data, indent=4))

Issuers can also retrieve a list of reissuances using the /assets/{assetUuid}/reissuances API, which shows transaction details for reissuances that have been confirmed on the Liquid network.

Assignment

Assignment is the act of pre-allocating amounts of an asset to investors for future distribution on the Liquid network. Issuers are free to modify an assignment as many times as necessary before it is marked for distribution. This is done by deleting the existing assignment and recreating it with amended amounts. Assignment does not require that investors have a wallet registered but, without it, any subsequent distribution cannot begin. Assignment does not change Liquid wallet balances and is only recorded within Liquid Securities.

# Assign some of the asset to the Investor
# api/assets/{asset_uuid}/assignments/create
# The assignment will be created as ready for distribution
# NOTE: Before the assignments can be made, the transaction in which the asset was issued
# needs to have had at least 2 confirmations on the Liquid blockchain or the assignment
# will fail with a warning about insufficient confirmed funds.
url = f'{API_URL}/assets/{asset_uuid}/assignments/create'
payload = {'assignments': [{
        'investor': investor_id,
        'amount': 1,
        'ready_for_distribution': True}]}
response = requests.post(url=url, data=json.dumps(payload), headers=headers)
assert response.status_code == 200
assignment = json.loads(response.text)
assignment_id = assignment[0]['id']
print('\nAsset Assignment:')
print('-----------------')
print(json.dumps(assignment, indent=4))

# Assignment details
# api/assets/{asset_uuid}/assignments/{assignment_id}
url = f'{API_URL}/assets/{asset_uuid}/assignments/{assignment_id}'
response = requests.get(url, headers=headers)
assert response.status_code == 200
assignment = json.loads(response.text)
print('\nAssignment details:')
print('-------------------')
print(json.dumps(assignment, indent=4))

# Investor summary
# api/investors/{investor_id}
# Balances and details of assignments, distributions for each asset held by the investor
url = f'{API_URL}/investors/{investor_id}/summary'
response = requests.get(url, headers=headers)
assert response.status_code == 200
investor_summary = json.loads(response.text)
print('\nInvestor summary:')
print('-----------------')
print(json.dumps(investor_summary, indent=4))

# Asset assignments list
# api/assets/{asset_uuid}/assignments
# The assignments for the asset. Expect this to be empty unless you have
# already created assignments.
url = f'{API_URL}/assets/{asset_uuid}/assignments'
response = requests.get(url, headers=headers)
assert response.status_code == 200
assignments = json.loads(response.text)
print('\nAsset assignments:')
print('------------------')
for assignment in assignments:
    print(json.dumps(assignment, indent=4))

Distribution

To allow distribution to begin, the asset must have first been registered with Blockstream Green as an Authorized Asset. The code to do this has already been shown above.

To start the distribution of the token on the Liquid network to investor wallets, a distribution is created, as shown in the code below. This marks the assignments as pending distribution and returns distribution data that must be run against the treasury’s node using the provided Python client. The client will perform the distribution, wait for sufficient blocks, and then confirm the distribution transaction by sending transaction information back to Liquid Securities. This ensures that asset ownership is tracked properly.

An Issuer can cancel the distribution at any point after the script is requested, as long as the script has not been executed and the distribution transaction has not been broadcast on the Liquid Network.

# Request the data needed to carry out a distribution.
# api/assets/{asset_uuid}/distributions/create/
# Returns json data that should be saved to file. The path to the file should be passed as an
# argument to a Python client that carries out the distribution itself. The client will be
# supplied to users of the api and will handle the distribution transactions and post back
# the resulting transaction data to the distributions/confirm endpoint after it has
# sufficient confirmations.
# NOTE: The url intentionally ends with a '/'
url = f'{API_URL}/assets/{asset_uuid}/distributions/create/'
response = requests.get(url, headers=headers)
assert response.status_code == 200
distribution_data = json.loads(response.text)
# The json data in distribution_data would then be saved to a file for use in the Python client
# that carries out the distribution itself.
print(json.dumps(distribution_data, indent=4))

Issuers can view distribution details of a specific distribution.

# The Assignment will now have a distribution_uuid:
# Assignment details
# api/assets/{asset_uuid}/assignments/{assignment_id}
url = f'{API_URL}/assets/{asset_uuid}/assignments/{assignment_id}'
response = requests.get(url, headers=headers)
assert response.status_code == 200
assignment = json.loads(response.text)
print('\nAssignment details:')
print('-------------------')
print(json.dumps(assignment, indent=4))

# Distribution details
# api/assets/{asset_uuid}/distributions/{distribution_uuid}
# Displays details for a distribution that has been distributed with a valid transaction using
# the Python client. Until the distribution transaction has confirmed the details api will
# return a 'not found' error.

Note

The following views show transaction details for distributions that have been confirmed by transactions on the Liquid network. For a list of pending distributions use the assignments list api.

url = f'{API_URL}/assets/{asset_uuid}/distributions/{distribution_uuid}'
response = requests.get(url, headers=headers)
assert response.status_code == 200
distribution = json.loads(response.text)
print('\nDistribution details:')
print('---------------------')
print(json.dumps(distribution, indent=4))

# Distributions list
# api/assets/{asset_uuid}/distributions
# Displays distributions that have been distributed with valid transactions
url = f'{API_URL}/assets/{asset_uuid}/distributions'
response = requests.get(url, headers=headers)
assert response.status_code == 200
distributions = json.loads(response.text)
print('\nDistribution:')
print('-------------')
for distribution in distributions:
    print(json.dumps(distribution, indent=4))

Balance, Summary, Ownership and Activities

A number of summary and balance type API are also available to help you manage asset treasury balances, ownership, and activities reporting.

# Asset summary
# api/assets/{assetUuid}/summary
# Balances and stats of an Asset
url = f'{API_URL}/assets/{asset_uuid}/summary'
response = requests.get(url, headers=headers)
assert response.status_code == 200
asset_summary = json.loads(response.text)
print('\nAsset summary:')
print('--------------')
print(json.dumps(asset_summary, indent=4))

# Asset activities
# api/assets/{assetUuid}/activities
url = f'{API_URL}/assets/{asset_uuid}/activities'
response = requests.get(url, headers=headers)
assert response.status_code == 200
asset_activities = json.loads(response.text)
print('\nAsset activities:')
print('-----------------')
print(json.dumps(asset_activities, indent=4))

# Asset ownerships
# api/assets/{assetUuid}/ownerships
url = f'{API_URL}/assets/{asset_uuid}/ownerships'
response = requests.get(url, headers=headers)
assert response.status_code == 200
asset_ownerships = json.loads(response.text)
print('\nAsset ownerships:')
print('-----------------')
print(json.dumps(asset_ownerships, indent=4))

# Asset balance
# api/assets/{assetUuid}/balance
url = f'{API_URL}/assets/{asset_uuid}/balance'
response = requests.get(url, headers=headers)
assert response.status_code == 200
asset_balance = json.loads(response.text)
print('\nAsset balance:')
print('--------------')
print(json.dumps(asset_balance, indent=4))

The complete python code example can be found here.

You can reference the API Specification for a full list and description of all the Liquid Securities API endpoints.