├── python3 ├── requirements.txt ├── sign.py ├── README.md └── test_program.py ├── LICENSE ├── README.md └── .gitignore /python3/requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography>=35.0.0,<42.0.0 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Nordnet Bank AB 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Nordnet API](https://www.nordnet.se/se/tjanster/handelsapplikationer#nordnet-api) 2 | [Read more about our trading API offering and more trading products on nordnet.se.](https://www.nordnet.se/se/tjanster/handelsapplikationer#nordnet-api) 3 | 4 | ## Disclaimer 5 | The code in this repo is intended as examples only. It is provided as is without 6 | any warranty of any kind, see `LICENSE` for more information. 7 | 8 | ## Examples 9 | * [Python 3](https://github.com/nordnet/next-api-v2-examples/tree/master/python3) 10 | 11 | ## Nordnet 12 | Nordnet is a pan-Nordic leading digital platform for savings and investments. Ever since we started in 1996, our purpose has been to democratize savings and investments. Through innovation, simplicity and transparency, we challenge traditional structures, and give private savers access to the same information, tools and services as professionals. With leading UX, cutting edge financial products, and automated and inspiring customer journeys, we are building the best platform for savings and investments. 13 | 14 | Nordnet has 800 employees, and we operate in Sweden, Norway, Denmark and Finland. 15 | 16 | Visit us at www.nordnetab.com, www.nordnet.se, www.nordnet.no, www.nordnet.dk or www.nordnet.fi. 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .idea 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | # mac 108 | *.DS_Store 109 | -------------------------------------------------------------------------------- /python3/sign.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Copyright 2025 Nordnet Bank AB 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | """ 22 | 23 | import base64 24 | import sys 25 | from cryptography.hazmat.backends import default_backend 26 | from cryptography.hazmat.primitives import serialization 27 | 28 | def ssh_key_sign(private_key_path, challenge): 29 | """ 30 | Calculate the signature of a challenge using an ed25519 private key 31 | 32 | Args: 33 | private_key_path: Path to your private key file (e.g., id_ed25519) 34 | challenge: The challenge to sign 35 | 36 | Returns: 37 | The base64 encoded signature 38 | """ 39 | # Load the private key 40 | try: 41 | with open(private_key_path, "rb") as key_file: 42 | private_key = serialization.load_ssh_private_key( 43 | key_file.read(), 44 | password=None, # If your key has a passphrase, provide it here 45 | backend=default_backend() 46 | ) 47 | except IOError: 48 | print(f"Could not find the following file: \"{private_key_path}\"") 49 | sys.exit() 50 | 51 | # Convert challenge string to bytes 52 | challenge_bytes = challenge.encode('utf-8') 53 | 54 | # Sign the challenge with the private key 55 | signature = private_key.sign( 56 | challenge_bytes, 57 | ) 58 | 59 | # Base64 encode the signature 60 | signature_b64 = base64.b64encode(signature).decode('utf-8') 61 | 62 | return signature_b64 63 | 64 | def main(): 65 | # Input path to your private key and the challenge to sign 66 | if len(sys.argv) != 3: 67 | raise Exception('To run test_program you need to provide as arguments [PRIVATE_KEY_PATH] [CHALLENGE_TO_SIGN]') 68 | private_key_path = sys.argv[1] 69 | challenge = sys.argv[2] 70 | 71 | signature = ssh_key_sign(private_key_path, challenge) 72 | 73 | print(signature); 74 | sys.exit(0) 75 | 76 | 77 | if __name__ == "__main__": 78 | main() 79 | -------------------------------------------------------------------------------- /python3/README.md: -------------------------------------------------------------------------------- 1 | ## Disclaimer 2 | The code in this repo is intended as examples only. It is provided as is without 3 | any warranty of any kind, see `LICENSE` for more information. 4 | 5 | _Note that all the JSON objects end with newline. As such you need to listen 6 | and read from the buffer when a full object has been transferred._ 7 | 8 | ## Requirements 9 | * There is no longer any test environment available. Contact Nordnet Trading Support to get started. 10 | * [Python 3](https://www.python.org/downloads/) and 11 | [pip](https://pip.pypa.io/en/stable/installation/) installed. 12 | 13 | ## Install and run 14 | 1. Download the `nordnet/next-api-v2-examples` repo 15 | 2. Run test_program.py and provide your API-key, country code and the path to your private key 16 | 17 | This repo also includes a small program called sign.py which takes a private key file and a 18 | challenge and prints the corresponding signature. This can be used to debug your 19 | own signature code. 20 | ``` 21 | cd python3 22 | pip3 install -r requirements.txt 23 | ./test_program.py [insert API-key] [insert country code] [insert private key file path] 24 | ``` 25 | Running the test program should output something that looks similar to the following example output 26 | ```json 27 | Checking Nordnet API status... 28 | << HTTP request GET /api/2/ 29 | { 30 | "message": "", 31 | "system_running": true, 32 | "timestamp": 1741781442953, 33 | "valid_version": true 34 | } 35 | Starting authentication challenge... 36 | << HTTP request POST /api/2/login/start 37 | { 38 | "challenge": "f0dcd2fa-92b1-4151-93af-61697eae217a" 39 | } 40 | Received challenge: f0dcd2fa-92b1-4151-93af-61697eae217a 41 | Completing authentication... 42 | << HTTP request POST /api/2/login/verify 43 | { 44 | "expires_in": 1800, 45 | "private_feed": { 46 | "encrypted": true, 47 | "hostname": "priv.next.nordnet.se", 48 | "port": 443 49 | }, 50 | "public_feed": { 51 | "encrypted": true, 52 | "hostname": "pub.next.nordnet.se", 53 | "port": 443 54 | }, 55 | "session_key": "15a6c4db-05b9-481c-b94a-ccffed83e693" 56 | } 57 | Successfully authenticated. Session key: 15a6c4db-05b9-481c-b94a-ccffed83e693 58 | 59 | Connecting to feed pub.next.prod.nordnet.se:443... 60 | 61 | << Sending cmd to feed: {'cmd': 'login', 'args': {'session_key': '15a6c4db-05b9-481c-b94a-ccffed83e693', 'service': 'NEXTAPI'}} 62 | << Sending cmd to feed: {'cmd': 'subscribe', 'args': {'t': 'price', 'm': 11, 'i': '101'}} 63 | 64 | Starting receiving from socket... 65 | 66 | >> JSON updates from public feed 67 | { 68 | "data": { 69 | "ask": 87.0, 70 | "ask_volume": 1200, 71 | "bid": 83.44, 72 | "bid_volume": 1, 73 | "close": 77.22, 74 | "high": 87.0, 75 | "i": "101", 76 | "id": 16750901, 77 | "last": 87.0, 78 | "last_volume": 154, 79 | "low": 82.96, 80 | "m": 11, 81 | "open": 83.12, 82 | "tick_timestamp": 1741781407194, 83 | "trade_timestamp": 1741780275120, 84 | "turnover": 8492556.03, 85 | "turnover_volume": 101883, 86 | "vwap": 84.56 87 | }, 88 | "type": "price" 89 | } 90 | ``` 91 | 92 | ## Common issues 93 | * SyntaxError: check that your Python version is 3 or higher 94 | 95 | ## Questions 96 | If you have technical questions then, 97 | 1. Check out the code, it is documented 98 | 2. Read the [API documentation](https://www.nordnet.se/externalapi/docs) 99 | -------------------------------------------------------------------------------- /python3/test_program.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Copyright 2018 Nordnet Bank AB 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | """ 22 | 23 | import base64 24 | import http.client 25 | import json 26 | import socket 27 | import ssl 28 | import sys 29 | import time 30 | import multiprocessing 31 | from cryptography.hazmat.backends import default_backend 32 | from cryptography.hazmat.primitives import serialization 33 | from cryptography.hazmat.primitives.asymmetric import padding 34 | from urllib.parse import urlencode 35 | 36 | 37 | # global variables with static information about Nordnet API 38 | VALID_COUNTRIES = {"se", "no", "dk", "fi"} 39 | API_URL = 'public.nordnet.' 40 | API_PREFIX = '/api' 41 | API_VERSION = '2' 42 | SERVICE_NAME = 'NEXTAPI' 43 | 44 | 45 | def ssh_key_authentication(conn, api_key, country_code, private_key_path): 46 | """ 47 | Authenticate using the new SSH key-based authentication flow 48 | 49 | Args: 50 | conn: An http.client connection object 51 | api_key: The API key provided by Nordnet 52 | country_code: The country code to add to domain (se, no, dk, or fi) 53 | private_key_path: Path to your private key file (e.g., id_ed25519) 54 | 55 | Returns: 56 | The session response data 57 | """ 58 | # 1. Start authentication challenge 59 | uri = f"{API_PREFIX}/{API_VERSION}/login/start" 60 | body = json.dumps({'api_key': api_key}) 61 | headers = { 62 | "Accept": "application/json", 63 | "Content-Type": "application/json" 64 | } 65 | 66 | print("Starting authentication challenge...") 67 | challenge_response = send_http_request(conn, 'POST', uri, body, headers) 68 | challenge = challenge_response["challenge"] 69 | print(f"Received challenge: {challenge}") 70 | 71 | # 2. Sign the challenge with the private key 72 | # Load the private key 73 | try: 74 | with open(private_key_path, "rb") as key_file: 75 | private_key = serialization.load_ssh_private_key( 76 | key_file.read(), 77 | password=None, # If your key has a passphrase, provide it here 78 | backend=default_backend() 79 | ) 80 | except IOError: 81 | print(f"Could not find the following file: \"{private_key_path}\"") 82 | sys.exit() 83 | 84 | # Sign the challenge 85 | from cryptography.hazmat.primitives.asymmetric import utils 86 | from cryptography.hazmat.primitives import hashes 87 | 88 | # Convert challenge string to bytes 89 | challenge_bytes = challenge.encode('utf-8') 90 | 91 | # Sign the challenge with the private key 92 | signature = private_key.sign( 93 | challenge_bytes, 94 | ) 95 | 96 | # Base64 encode the signature 97 | signature_b64 = base64.b64encode(signature).decode('utf-8') 98 | 99 | # 3. Complete the authentication 100 | uri = f"{API_PREFIX}/{API_VERSION}/login/verify" 101 | body = json.dumps({ 102 | 'service': SERVICE_NAME, 103 | 'api_key': api_key, 104 | 'signature': signature_b64 105 | }) 106 | headers = { 107 | "Accept": "application/json", 108 | "Content-Type": "application/json" 109 | } 110 | 111 | print("Completing authentication...") 112 | login_response = send_http_request(conn, 'POST', uri, body, headers) 113 | 114 | return login_response 115 | 116 | def send_http_request(conn, method, uri, params, headers): 117 | """ 118 | Send a HTTP request 119 | """ 120 | conn.request(method, uri, params, headers) 121 | r = conn.getresponse() 122 | print("<< HTTP request " + method + " " + uri) 123 | response = r.read().decode('utf-8') 124 | j = json.loads(response) 125 | print(json.dumps(j, indent=4, sort_keys=True)) 126 | return j 127 | 128 | def connect_to_feed(public_feed_hostname, public_feed_port): 129 | """ 130 | Connect to the feed and get back a TCP socket 131 | """ 132 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 133 | if public_feed_port == 443: 134 | c = ssl.create_default_context() 135 | s = c.wrap_socket(s, server_hostname=public_feed_hostname) 136 | s.connect((public_feed_hostname, public_feed_port)) 137 | return s 138 | 139 | def send_cmd_to_socket(socket, cmd): 140 | """ 141 | Send commands to the feed through the socket 142 | """ 143 | socket.send(bytes(json.dumps(cmd) + '\n', 'utf-8')) 144 | print("<< Sending cmd to feed: " + str(cmd)) 145 | 146 | def try_parse_into_json(string): 147 | """ 148 | Try parsing the string into JSON objects. Return the unparsable 149 | parts as buffer 150 | """ 151 | json_strings = string.split('\n') 152 | 153 | for i in range(0, len(json_strings)): 154 | try: 155 | json_data = json.loads(json_strings[i]) 156 | print(">> JSON updates from public feed") 157 | print(json.dumps(json_data, indent=4, sort_keys=True)) 158 | except: 159 | ## If this part cannot be parsed into JSON, It's probably not 160 | ## complete. Stop it right here. Merge the rest of list and 161 | ## return it, parse it next time 162 | return ''.join(json_strings[i:]) 163 | 164 | ## If all JSONs are successfully parsed, we return an empty buffer 165 | return '' 166 | 167 | def do_receive_from_socket(socket, last_buffer): 168 | """ 169 | Receive data from the socket, and try to parse it into JSON. Return 170 | the unparsable parts as buffer 171 | """ 172 | # Consume message (price data or heartbeat) from public feed 173 | #> Note that a full message with all the JSON objects ends with a 174 | #> newline symbol "\n". As such you need to listen and read from 175 | #> the buffer until a full message has been transferred 176 | time.sleep(0.01) 177 | new_data = socket.recv(1024).decode('utf-8') 178 | 179 | string = last_buffer + new_data 180 | if string != '': 181 | new_buffer = try_parse_into_json(string) 182 | return new_buffer 183 | 184 | return '' 185 | 186 | def receive_message_from_socket(socket): 187 | """ 188 | Receive data from the socket and parse it 189 | """ 190 | print('\nStarting receiving from socket...\n') 191 | buffer = '' 192 | while True: 193 | buffer = do_receive_from_socket(socket, buffer) 194 | print('\nFinishing receiving from socket...\n') 195 | 196 | 197 | def main(): 198 | 199 | # Input API key string (from uploading your public key on www.nordnet.se|dk|no|fi), country code (se|dk|no|fi) and path to your private key 200 | if len(sys.argv) != 4: 201 | raise Exception('To run test_program you need to provide as arguments [API_KEY] [COUNTRY_CODE] [PRIVATE_KEY_PATH]') 202 | api_key = sys.argv[1] 203 | country_code = sys.argv[2].lower() 204 | private_key_path = sys.argv[3] 205 | 206 | if country_code not in VALID_COUNTRIES: 207 | raise Exception(f"COUNTRY_CODE parameter must be one of 'se', 'no', 'dk' or 'fi'.") 208 | 209 | # Create an HTTPS connection 210 | conn = http.client.HTTPSConnection(API_URL + country_code) 211 | headers = {"Accept": "application/json"} 212 | 213 | # Check Nordnet API status. Check Nordnet API documentation page to verify the path 214 | print("Checking Nordnet API status...") 215 | uri = API_PREFIX + '/' + API_VERSION + '/' 216 | j = send_http_request(conn, 'GET', uri, '', headers) 217 | 218 | # Login using SSH key authentication 219 | j = ssh_key_authentication(conn, api_key, country_code, private_key_path) 220 | 221 | # Store Nordnet API login response data 222 | public_feed_hostname = j["public_feed"]["hostname"] 223 | public_feed_port = j["public_feed"]["port"] 224 | our_session_key = j["session_key"] 225 | 226 | print(f"Successfully authenticated. Session key: {our_session_key}") 227 | 228 | # Establish connection to public feed 229 | print("\nConnecting to feed " + str(public_feed_hostname) + ":" + str(public_feed_port) + "...\n") 230 | feed_socket = connect_to_feed(public_feed_hostname, public_feed_port) 231 | 232 | # Start a parallel process that keeps receiving updates from the TCP socket 233 | multiprocessing.set_start_method('fork') 234 | proc = multiprocessing.Process(target=receive_message_from_socket, args=(feed_socket,)) 235 | proc.start() 236 | 237 | # Login to public feed with our session_key from Nordnet API response 238 | cmd = {"cmd": "login", "args": {"session_key": our_session_key, "service": SERVICE_NAME}} 239 | send_cmd_to_socket(feed_socket, cmd) 240 | 241 | # Subscribe to ERIC B price in public feed 242 | cmd = {"cmd": "subscribe", "args": {"t": "price", "m": 11, "i": "101"}} 243 | send_cmd_to_socket(feed_socket, cmd) 244 | 245 | console_input = input() 246 | while console_input != "exit": 247 | console_input = input() 248 | 249 | feed_socket.shutdown(socket.SHUT_RDWR) 250 | feed_socket.close() 251 | proc.terminate() 252 | sys.exit(0) 253 | 254 | main() 255 | --------------------------------------------------------------------------------