Investment Platform API

This guide shows you how to use the API to create an investor and then create an investment for that investor. The API allows you to collect information about the investment process in order to document the compliant brokerage of financial products.

In the course of this guide, we will create a simple script that shows the basic interactions with the API. At the end of this guide, you will also find the complete script for you to copy, paste and try out. Remember to insert your account specific information before running the script.

Getting started

First we are going to import the packages that we need and set up some basic variables. The client ID and client secret will be provided to you. The campaign ID is set up for you as well during the product creation.

import binascii
import os

import requests

URL = 'https://demo.cashlink.de/api/external'

CLIENT_ID = '<your client ID>'
CLIENT_SECRET = '<your client secret>'

CAMPAIGN_ID = '<campaign id>'

Now we write a helper function that follows the OAuth protocol to retrieve an access token that we will use to authenticate the API calls.

def authenticate():
    r = requests.post(f'{URL}/oauth/token', data={
        'grant_type': 'client_credentials',
        'scope': 'investors:write investors:read investments:write investments:read documents:write',
        'client_id': CLIENT_ID,
        'client_secret': CLIENT_SECRET
    })
    assert r.status_code == 200, r.content
    return r.json()['access_token']

We add an additional helper function that with every call creates a random idempotency key. In your application, you would use an idempotency key that correlates to the internal object on your side.

def make_key():
    return binascii.hexlify(os.urandom(8)).decode()

Create an investor

As we now can access the API we use it to create an investor. Besides personal data, the investor also needs a valid identification to be able to invest. If you've already identified the user, you can upload the data here. Depending on your setup, the investor might need to go through an identification process again. See Investors -> Identification for more details.

if __name__ == '__main__':
    headers = {
        'Authorization': f'Bearer {authenticate()}',
    }

    # Create investor
    r = requests.post(
        f'{URL}/v2/investors/',
        json={
            "natural_person": {
                "salutation": "MR",
                "forename": "Peter",
                "surname": "Parker",
                "birth_date": "2000-08-10",
                "birth_place": "New York",
                "citizenship": "DEU",
                "street": "Hauptstr. 37",
                "city": "Frankfurt",
                "zip": "60316",
                "country": "DEU",
                "phone": "+49 123 456 789",
                "is_pep": False
            },
            "bank_account": {
                "account_holder": "Peter Parker",
                "iban": 'DE53500105173569146251',
                "bic": "GENODEF1ERG",
                "bank": "Commerzbank Frankfurt",
                "country": "DEU",
                "currency": "EUR"
            },
            "tax_information": {
                "tax_identification_number": "12345678",
                "non_assessment_certificate": True
            },
            "communication": {
                "email": "Insert test email address here ...",
                "email_confirmed": True
            },
            "is_beneficiary": True,
            "account_setup_accepted_at": "2021-05-06T07:14:13.630308+00:00"
        },

        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content
    investor_id = r.json()['id']
    
    # Upload the legitimation protocol document to reference it later
    r = requests.post(
        f'{URL}/v2/documents/',
        files={'file': ('protocol.pdf', b'ASD', 'application/pdf')},
        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content
    doc_id = r.json()['id']
    
    # Create the identification for investor
    r = requests.post(
        f'{URL}/v2/investors/{investor_id}/identifications/',
        json={
            "document_type": "IDCARD",
            "document_id": "ID123456",
            "document_issuer": "Buergeramt Frankfurt",
            "document_valid_from": "2020-08-10",
            "document_valid_to": "2025-08-10",
            "identity_provider": "POSTIDENT",
            "legitimation_process_id": "string",
            "legitimation_protocol_id": doc_id,
            "verified_at": "2020-08-10T14:06:22.134Z"
        },
        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content
    identification_id = r.json()['id']

If you want to onboard a legal person as an investor (e.g. a GmbH / AG) please follow this guide for a more detailed explanation.

Add survey answers

In addition, we need to store the survey answers for the investor. See Investors -> Knowledge and experiences survey for more details.

    # Store the survey answers with the investor
    r = requests.post(
        f'{URL}/v2/investors/{investor_id}/survey/',
        json = {
            "survey_submitted_at": "2022-03-22T07:14:13.630308+00:00",
            "survey": []
        },
        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content

Create a wallet

Investors need a wallet to receive the digital securities. In this example, we create a managed wallet for the investor. That means that the private key will remain in secure custody. However, you could also add a custom account address of a self-custody wallet belonging to the investor.

    # Create a wallet for investor
    r = requests.post(
        f'{URL}/v2/investors/{investor_id}/wallets/eth-managed/',
        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content
    wallet_id = r.json()['id']

Create an invitation

With the investor ID at hand, we create an invitation for this investor.

    # Create a basic invitation with campaign, investor and units
    r = requests.post(
        f'{URL}/v2/invitations/',
        json={
            "investor_id": investor_id,
            "campaign_id": CAMPAIGN_ID,
            "units": 100
            },
        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content
    invitation_id = r.json()['id']

Set the survey waiver

Based on the answers to the knowledge and experience survey set for the investor, the API will return a value for the required survey waiver. In your frontend, display the survey waiver to the investor and update via the API once the investor has accepted the waiver.

    # Patch the invitation with the required survey waiver
    r = requests.patch(
        f'{URL}/v2/invitations/{invitation_id}/',
        json = {
            "survey_waiver": "NO_ANSWERS",
        },
        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content

Create an investment

Once the survey waiver is set we can use the invitation ID to create an investment.

    # Use the invitation to create an investment
    r = requests.post(
        f'{URL}/v2/investments/',
        json={
            "invitation_id": invitation_id,
            "signed_at": "2022-03-22T08:33:45.617Z"
        },
        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content
    investment_id = r.json()['id']

Assign wallet to investment

The investment needs a wallet. The digital securities will be credited to the wallet's account address.

    # Patch investment to set wallet
    r = requests.patch(
        f'{URL}/v2/investments/{investment_id}/',
        json={
            "wallet": wallet_id
        },
        headers={**headers}
    )
    assert r.status_code < 300, r.content

Mark investment as paid

After the payment has been received for the investment, it needs to set to paid by providing the date and time of the payment.

    # Patch investment to set payment date
    r = requests.patch(
        f'{URL}/v2/investments/{investment_id}/',
        json={
            "paid_at": "2022-03-23T08:33:45.617Z"
        },
        headers={**headers}
    )
    assert r.status_code < 300, r.content

You might receive an error response here, if you need to manually accept the investment via the Cashlink Studio. In this case, follow the description here to accept the investment.

Credit digital securities

Once the payment date is set and the investment has an assigned wallet, you can credit the digital securities to the wallet.

    # Initiate credit of digital securities
    r = requests.post(
        f'{URL}/v2/investments/{investment_id}/credits/',
        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content

Retrieve status information

We can now use the investments endpoint to retrieve all information about the investment.

    # Retrieve investment status
    r = requests.get(
        f'{URL}/v2/investments/{investment_id}/',
        headers={**headers}
    )
    assert r.status_code < 300, r.content
    status = r.json()['status']
    print(f'Successfully created investment with ID {investment_id} [{status}].')

Full script

This is how the full script will look like.

import binascii
import os
import requests

URL = 'https://demo.cashlink.de/api/external'
CLIENT_ID = 'Insert your client ID here ...'
CLIENT_SECRET = 'Insert your client secret here ...'

CAMPAIGN_ID = 'Insert the campaign ID here ...'


def authenticate():
    r = requests.post(f'{URL}/oauth/token', data={
        "grant_type": "client_credentials",
        "scope": 'investors:write investors:read investments:write investments:read documents:write',
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET
    })
    assert r.status_code == 200, r.content
    return r.json()['access_token']


def make_key():
    return binascii.hexlify(os.urandom(8)).decode()


if __name__ == '__main__':
    headers = {
        'Authorization': f'Bearer {authenticate()}',
    }

    # Create investor
    r = requests.post(
        f'{URL}/v2/investors/',
        json={
            "natural_person": {
                "salutation": "MR",
                "forename": "Peter",
                "surname": "Parker",
                "birth_date": "2000-08-10",
                "birth_place": "New York",
                "citizenship": "DEU",
                "street": "Hauptstr. 37",
                "city": "Frankfurt",
                "zip": "60316",
                "country": "DEU",
                "phone": "+49 123 456 789",
                "is_pep": False
            },
            "bank_account": {
                "account_holder": "Peter Parker",
                "iban": 'DE53500105173569146251',
                "bic": "GENODEF1ERG",
                "bank": "Commerzbank Frankfurt",
                "country": "DEU",
                "currency": "EUR"
            },
            "tax_information": {
                "tax_identification_number": "12345678",
                "non_assessment_certificate": True
            },
            "communication": {
                "email": "Insert test email address here ...",
                "email_confirmed": True
            },
            "is_beneficiary": True,
            "account_setup_accepted_at": "2021-05-06T07:14:13.630308+00:00"
        },

        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content
    investor_id = r.json()['id']

    # Upload the legitimation protocol document to reference it later
    r = requests.post(
        f'{URL}/v2/documents/',
        files={'file': ('protocol.pdf', b'ASD', 'application/pdf')},
        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content
    doc_id = r.json()['id']

    # Create the identification for investor
    r = requests.post(
        f'{URL}/v2/investors/{investor_id}/identifications/',
        json={
            "document_type": "IDCARD",
            "document_id": "ID123456",
            "document_issuer": "Buergeramt Frankfurt",
            "document_valid_from": "2020-08-10",
            "document_valid_to": "2025-08-10",
            "identity_provider": "POSTIDENT",
            "legitimation_process_id": "string",
            "legitimation_protocol_id": doc_id,
            "verified_at": "2020-08-10T14:06:22.134Z"
        },
        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content
    identification_id = r.json()['id']

    # Store the survey answers with the investor
    r = requests.post(
        f'{URL}/v2/investors/{investor_id}/survey/',
        json = {
            "survey_submitted_at": "2022-03-22T07:14:13.630308+00:00",
            "survey": []
        },
        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content

    # Create a wallet for investor
    r = requests.post(
        f'{URL}/v2/investors/{investor_id}/wallets/eth-managed/',
        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content
    wallet_id = r.json()['id']

    # Create a basic invitation with campaign, investor and units
    r = requests.post(
        f'{URL}/v2/invitations/',
        json={
            "investor_id": investor_id,
            "campaign_id": CAMPAIGN_ID,
            "units": 100
        },
        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content
    invitation_id = r.json()['id']

    # Patch the invitation with the required survey waiver
    r = requests.patch(
        f'{URL}/v2/invitations/{invitation_id}/',
        json = {
            "survey_waiver": "NO_ANSWERS",
        },
        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content

    # Use the invitation to create an investment
    r = requests.post(
        f'{URL}/v2/investments/',
        json={
            "invitation_id": invitation_id,
            "signed_at": "2022-03-22T08:33:45.617Z"
        },
        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content
    investment_id = r.json()['id']

    # Patch investment to set wallet
    r = requests.patch(
        f'{URL}/v2/investments/{investment_id}/',
        json={
            "wallet": wallet_id
        },
        headers={**headers}
    )
    assert r.status_code < 300, r.content

    # Patch investment to set payment date
    r = requests.patch(
        f'{URL}/v2/investments/{investment_id}/',
        json={
            "paid_at": "2022-03-23T08:33:45.617Z"
        },
        headers={**headers}
    )
    assert r.status_code < 300, r.content

    # Initiate credit of digital securities
    r = requests.post(
        f'{URL}/v2/investments/{investment_id}/credits/',
        headers={**headers, 'X-Idempotency-Key': make_key()}
    )
    assert r.status_code < 300, r.content

    # Retrieve investment status
    r = requests.get(
        f'{URL}/v2/investments/{investment_id}/',
        headers={**headers}
    )
    assert r.status_code < 300, r.content
    status = r.json()['status']
    print(f'Successfully created investment with ID {investment_id} [{status}].')

Last updated