Creating investments via distribution platforms

Distribution platforms can create investments as part of a campaign. Each product can have one or more campaigns for primary market distribution. A product’s total allocation can be divided across multiple campaigns, but the combined allocation of all campaigns cannot exceed the product’s total allocation.

This guide explains the workflow for issuers and distribution platforms in two steps:

  1. The issuer creates a campaign for a distribution platform.

  2. The distributor uses that campaign to create investments

Creating a campaign via the Issuer API

Before creating a campaign via the Issuer API, make sure that:

  1. You have created a product and have its ID.

  2. You have the ID of the target distribution platform.

  3. The product is listed on that distribution platform

The following data is needed to create a campaign:

Field
Description

distribution_platform_id

ID of the distribution platform where the product is listed

name

Name of the campaign

allocation

Total amount that can be used for investments with that campaign

price_per_token

The purchase price for one unit of the crypto security

min_number_of_tokens

Minimum amount of crypto securities that must be purchased in one investment

max_number_of_tokens

Maximum amount of crypto securities that can be purchased in one investment

legal_persons_allowed

If set to true, legal persons are allowed to invest via this campaign

natural_persons_allowed

If set to false, legal persons are allowed to invest via this campaign

country

Country where the crypto securities should be offered.

rule_type

Two rule types can be set:

MINIMUM INVESTMENT AMOUNT: Only allows investments starting at a specific investment amount

PUBLIC OFFERING: No restrictions apply to the creation of new investments.

is_campaign_active

If set to True, the distribution platform can create investments for the campaign. If set to False no new investments can be made.

Example:

# Create campaign
r = requests.post(
    f'{URL}/issuer/v2/products/{product_id}/campaigns/',
    json={
        'distribution_platform_id': distribution_platform_id,
        'allocation': 10000,
        'price_per_token': {
            'amount': '100',
            'decimals': 2,
            'currency': 'EUR'
        },
        'min_number_of_tokens': 1,
        'max_number_of_tokens': 100,
        'name': 'Test',
        'legal_persons_allowed': True,
        'natural_persons_allowed': True,
        'country': 'DEU',
        'rule_type': 'public_offering',
        'is_campaign_active': True
    },
    headers={**headers, 'X-Idempotency-Key': make_key()}
)
assert r.status_code < 300, r.content
print(f'Campaign: {r.json()['id']}')

After creating a campaign the only value that can be changed is the is_campaign_active field. If any other data of a campaign needs to be changed after initial creation, a new campaign must be created and the old one must be set to inactive.

Creating investments via the Distributor API

Investments can be created and processed via the investment endpoints.

Retrieving campaign information

Using the campaigns endpoint you can retrieve information about campaigns that have been created for your distribution platform. Only if such campaigns are named there, you will be able to create investments for that campaigns.

Example:

# Get all products listed on the distribution platform
r = requests.get(
    f'{URL}/distributor/v2/products/',
    headers={**headers}
)
assert r.status_code < 300, r.content
product_id = r.json()['results'][0]['id']

r = requests.get(
    f'{URL}/distributor/v2/products/{product_id}/',
    headers={**headers}
)
assert r.status_code < 300, r.content
print(f'Product: {r.json()['id']}')

# Get all campaigns for that product
r = requests.get(
    f'{URL}/distributor/v2/products/{product_id}/campaigns/',
    headers={**headers}
)
assert r.status_code < 300, r.content
campaign_id = r.json()['results'][0]['id']

r = requests.get(
    f'{URL}/distributor/v2/products/{product_id}/campaigns/{campaign_id}/',
    headers={**headers}
)
assert r.status_code < 300, r.content
print(f'Campaign: {r.json()['id']}')

Creating an investment

To create an investment you need at least the following data:

  • The ID of the investor the investment is created for (investor_id)

  • The ID of the campaign the investor wants to invest in (campaign_id)

  • The amount of units of the investment (units)

  • The date and time when the investor gave a binding confirmation to invest (signed_at)

  • A set of signed documents the investor has signed during the investment process (see Signed documents).

Signed documents

Investments involve signed documents that legally describe the investment contract with the issuer (such as a subscription form). You can access the signed documents via the API's signed documents endpoint at any time.

You can upload documents and add them to the investment via the signed documents endpoint. We require the following documents:

Document
Subject
Category

Signed Cashlink terms of service and data protection terms

effecta_cryptoregistry_tos

effecta_cryptoregistry_tos

Signed issuance document (e.g. bond conditions)

effecta_document

effecta_document

Acceptance by the issuer

All investments need to be marked as accepted by the issuer before they can be processed further. This can either be done via the Cashlink Studio or programmatically via the Distributor API.

Example:

r = requests.patch(
    f'{URL}/distributor/v2/investments/{investment_id}/',
    json={
        'accepted_at': '2025-03-12T07:14:13.630308+00:00',
    },
    headers={**headers, 'X-Idempotency-Key': make_key()}
)
assert r.status_code < 300, r.content
print(f'ID {investment_id} [{r.json()['status']}].')

You may also already provide the accepted_at in the request to create an investment.

Mark investment as paid

You can mark investments as paid via the API by adding a payment date to the investment via the investments endpoint after the investment has been marked as accepted by the issuer. That means that the accepted_at field is set.

The payment amount is calculated based on the investment amount, agio, disagio and accrued interest. The investment amount is calculated based on the units value set on the investment multiplied by the price of the campaign. The disagio amount can be set for an investment via the disagio field. The agio is a fixed setting set for a specific campaign. The accrued interest is calculated based on the product configuration like interest rate and interest periods.

Please note that the accrued interest and therefore the total payable amount can change until the investment has been marked as accepted by the issuer. Show the total payment amount to investors only after the investment has been accepted.

Example:

r = requests.patch(
    f'{URL}/distributor/v2/investments/{investment_id}/',
    json={
        'paid_at': '2025-03-12T07:14:13.630308+00:00'
    },
    headers={**headers, 'X-Idempotency-Key': make_key()}
)
assert r.status_code < 300, r.content
print(f'ID {investment_id} [{r.json()['status']}].')

Wallet association

For an investment to settle on-ledger, the investor's wallet used for this particular investment is required. Adding the wallet ID to the investment will associate the wallet with the investment. The wallet association can be updated as long as the investment's issuance transaction has not been triggered yet.

Example:

r = requests.patch(
    f'{URL}/distributor/v2/investments/{investment_id}/',
    json={
        'wallet': wallet_id
    },
    headers={**headers, 'X-Idempotency-Key': make_key()}
)
assert r.status_code < 300, r.content
print(f'ID {investment_id} [{r.json()['status']}].')

Crediting crypto securities

You are able to trigger the crediting of crypto securities using the /credits endpoint. The crediting can only be triggered if the investment has been marked as accepted and paid and if a wallet has been assigned to the investment.

Example:

r = requests.post(
    f'{URL}/distributor/v2/investments/{investment_id}/credits/',
    headers={**headers, 'X-Idempotency-Key': make_key()}
)
assert r.status_code < 300, r.content
print(f'Credit: {r.json()['id']}')

The crediting triggers an issuance transaction on-ledger increasing the balance of the wallet associated with the investment by the purchased number of units.

The response contains a status field with the following codes:

  • processing: The task is created and is currently being executed

  • failed: The task could not be completed

  • succeeded: The task was completed successfully

  • unknown: The task needs manual investigation to determine if the transaction has actually succeeded

If the task failed because the issuance transaction on-ledger could not be executed, the error message indicates that. If the task succeeded, the status of the investment is updated to DELIVERED.

Cancellations

Using the cancellation endpoint, you can submit the request of an investor for an investment to be aborted. The investment will receive the status ABORTED.

Example:

r = requests.post(
    f'{URL}/distributor/v2/investments/{investment_id}/cancellation/',
    json={
        'cancellation_date': '2025-03-12T07:14:13.630308+00:00'
    },
    headers={**headers, 'X-Idempotency-Key': make_key()}
)
assert r.status_code < 300, r.content
print(f'ID {investment_id} [{r.json()['status']}].')

The investment can be canceled by the investor if the investment is in one of the following states:

  • KYC_PENDING

  • WAITING_FOR_ACCEPTANCE

If the investment cannot be aborted via API, an error will be returned to the client.

Using the Cashlink Studio, there are additional ways to cancel an investment. This includes the following cases:

  • The KYC/AML process could not be completed in time

  • The payment has not been received in full and in time

  • The issuer has rejected the investment

  • After payment the investment can be cancelled if requested by the investor as long as the investment has not been transferred to any other investor

Last updated

Was this helpful?