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:
The issuer creates a campaign for a distribution platform.
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:
You have created a product and have its ID.
You have the ID of the target distribution platform.
The product is listed on that distribution platform
The following data is needed to create a campaign:
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:
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.
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?