# Saxo OpenAPI quickstart: simulation account to first call

Independent and unofficial. Not affiliated with or endorsed by any broker or platform named here. Educational only: nothing here is advice, and nothing here recommends a broker or a trade.

This is the markdown version of https://openbrokerapi.com/brokers/saxo/quickstart, written from the same records as that page.

Risk warning: CFDs and spread bets are complex instruments and come with a high risk of losing money rapidly due to leverage. You can lose money on these products, and each provider publishes its own loss figure under FCA rules. You should consider whether you understand how they work and whether you can afford to take the high risk of losing your money. Automated and API trading can amplify losses through bugs, connectivity failures and runaway orders.

Saxo OpenAPI quickstart: a free simulation account, an app key, the 24 hour developer portal token, the SIM gateway URL and a first GET on /port/v1/users/me. Every step sourced on Saxo's own developer pages.

- Checked: 2026-09-19

Unverified: the code on this page was written from the documentation and has not yet been run on a demo account.

Demo accounts only. Credentials are read from environment variables on your own machine. No field on this site asks for a key, a username or a password.

## Steps

### 1. Get a simulation account, then a portal account

Saxo's developer portal asks you to create an account on the portal to use its online tools, and says that if you do not have a Saxo simulation account you should request one from the signup link on the same page. Simulation comes first: the whole of this quickstart happens there.

Status: verified. Source: [developer.saxo](https://www.developer.saxo/openapi/learn), checked 2026-09-19.

### 2. Create an application and take the key and secret

The portal's Application Management section is where you create an application that can reach the demo trading environment. It gives you an application key and an application secret. Saxo's security page says not to expose either to your users, and that the key and secret are not shared between simulation and live: a live application is a separate application.

Status: verified. Source: [developer.saxo](https://www.developer.saxo/openapi/learn/security), checked 2026-09-19.

### 3. Take the 24 hour token instead of writing OAuth today

The portal issues a one day access token so you can start calling the API without building a full login flow first. Saxo is explicit that a one day token obtained this way is authorised for the simulation environment only. Live needs one of the OAuth flows: authorization code for server side web apps, authorization code with PKCE for native apps, or certificate based authentication for selected partners.

Status: verified. Source: [developer.saxo](https://www.developer.saxo/openapi/learn/security), checked 2026-09-19.

### 4. Point at the simulation gateway, not the live one

Simulation REST calls go to https://gateway.saxobank.com/sim/openapi and simulation authentication to https://sim.logonvalidation.net. Live is https://gateway.saxobank.com/openapi with https://live.logonvalidation.net. Sending a simulation token to the live gateway fails: they are different environments with different credentials.

Status: verified. Source: [developer.saxo](https://www.developer.saxo/openapi/learn/environments), checked 2026-09-19.

### 5. Make the first call: who am I

GET /port/v1/users/me returns details about the logged in user, which makes it the cheapest proof that your token works. The Authorization header carries the token as a bearer token. A 401 here means the token, not the endpoint.

Python:

```python
# Set SAXO_SIM_TOKEN as an environment variable first.
# Never paste a real token into code or into this site.
import os
import requests

BASE_URL = "https://gateway.saxobank.com/sim/openapi"
TOKEN = os.environ["SAXO_SIM_TOKEN"]

resp = requests.get(
    BASE_URL + "/port/v1/users/me",
    headers={"Authorization": "Bearer " + TOKEN, "Accept": "application/json"},
    timeout=10,
)
resp.raise_for_status()
me = resp.json()
print(me.get("UserId"), me.get("Language"), me.get("ClientKey"))
# Simulation only. A one day portal token is not valid on the live gateway.
```

curl:

```bash
# Set SAXO_SIM_TOKEN as an environment variable first.
# Never paste a real token into code or into this site.
curl -i "https://gateway.saxobank.com/sim/openapi/port/v1/users/me" \
  -H "Authorization: Bearer $SAXO_SIM_TOKEN" \
  -H "Accept: application/json"
```

Status: verified. Source: [developer.saxo](https://www.developer.saxo/openapi/referencedocs/port/v1/users/getuser), checked 2026-09-19.

### 6. Read the limits before you write a loop

Saxo's published defaults are 10,000,000 requests a day per application, 120 requests a minute per session per service group, and one order a second per session. Responses carry X-RateLimit headers for limit, remaining and reset, and going over returns 429. Checked 18 Sep 2026.

Status: verified. Source: [developer.saxo](https://www.developer.saxo/openapi/learn/rate-limiting), checked 2026-09-19.

### 7. From first call to first order

One Python file built from the Saxo OpenAPI endpoint records on this site, as the last step of this quickstart. It reads SAXO_SIM_TOKEN from the environment. GET /port/v1/positions/me (version v1) reads your open positions. POST /trade/v2/orders (version v2) places one small demo order with every field the record marks required (AccountKey, Uic, AssetType, BuySell, Amount, OrderType and OrderDuration). The record says its required marks are this site's reading of the page and not the page's own marks, and the script carries that in its comments. GET /port/v1/orders/{ClientKey}/{OrderId} (version v1) reads the confirmation or status. It sends at most one request every second, which stays under each request limit in the Saxo OpenAPI limits table below. It refuses to run against any host but https://gateway.saxobank.com/sim/openapi, and it never prints a token. Dry run is the default: save it as saxo_first_order.py, run python saxo_first_order.py, and it prints each request it would send with every secret shown as the name of its variable, reading no credential at all. Run python saxo_first_order.py send to send them to the demo host. The script has not been run against Saxo OpenAPI.

Python:

```python
#!/usr/bin/env python3
"""Saxo OpenAPI demo API: from first call to first order.

Unofficial example from Broker API Hub, not published by Saxo OpenAPI. Written from the Saxo OpenAPI endpoint records on
openbrokerapi.com and not run against Saxo OpenAPI: the quickstart page says so. Demo host only.

What it does, in order:
  1. proves the token with GET /port/v1/accounts/me (version v1)
  2. GET /port/v1/clients/me (version v1): reads ClientKey
  3. GET /port/v1/positions/me (version v1): reads your open positions
  4. POST /trade/v2/orders (version v2): places one small demo order with every field the record marks required
  5. GET /port/v1/orders/{ClientKey}/{OrderId} (version v1): reads the confirmation or status of that order

Dry run is the default: nothing is sent, no credential is read, and each request is printed as it
would go, with every secret shown as the name of the variable that holds it. Run it with the word
send as the first argument (python saxo_first_order.py send) to send the requests to the demo host.

Before a real send, set these environment variables on your own machine:
  SAXO_SIM_TOKEN   the 24 hour token from the developer portal, simulation only
Never paste real values into this file or into any website.
Needs Python 3.8 or later and requests (pip install requests).
"""
import json
import os
import sys
import time
import uuid

import requests

DEMO_HOST = "https://gateway.saxobank.com/sim/openapi"
BASE_URL = os.environ.get("SAXO_BASE_URL", DEMO_HOST).rstrip("/")
SEND = len(sys.argv) > 1 and sys.argv[1] == "send"
# Pacing, from this site's limits register for Saxo OpenAPI:
#   120/min per session, per service group (session; checked 18 Sep 2026)
# One request every second stays under each of them.
MIN_GAP = 1

if BASE_URL != DEMO_HOST:
    sys.exit("Refusing to run: this script only talks to the demo host " + DEMO_HOST)

# The order. Every field the order record marks required is here, with the record's own words after it,
# and so is every field the vendor's page marks neither required nor optional (tagged not marked by the vendor).
# A value of "..." or 0 is a placeholder: set it before a real send, or the script refuses to send.
ORDER = {
    "AccountKey": "...",                            # Account to place the order on, from GET /port/v1/accounts/me. (required)
    "Uic": 0,                                       # Unique id of the instrument. (required)
    "AssetType": "...",                             # The instrument's asset type. (required)
    "BuySell": "Buy",                               # Buy or Sell. (required)
    "Amount": 0,                                    # Order size. (required)
    "OrderType": "Market",                          # Placeable order type, for example Market, Limit or Stop. (required)
    "OrderDuration": {"DurationType": "DayOrder"},  # Duration, for example DurationType DayOrder or GoodTillCancel. (required)
}


def env(name):
    """A credential or setting. In a dry run the value is the variable's own name, so nothing secret is read or printed."""
    if not SEND:
        return "<" + name + ">"
    value = os.environ.get(name)
    if not value:
        sys.exit("Set the " + name + " environment variable first.")
    return value


# Session and credential headers live in this object's memory only: never printed, never saved.
http = requests.Session()
http.headers.update({
    "Authorization": "Bearer " + env("SAXO_SIM_TOKEN"),
    "Accept": "application/json",
})
last_call = 0.0


def request(method, path, version=None, body=None, params=None):
    """One request: printed in a dry run, paced and sent otherwise. Exits on an error reply, without printing a token."""
    global last_call
    headers = {}
    if version:
        headers["VERSION"] = version
    headers["x-request-id"] = str(uuid.uuid4())  # a fresh id per request, as the record asks
    if not SEND:
        print("DRY RUN " + method + " " + BASE_URL + path)
        shown = dict(http.headers)
        shown.update(headers)
        print("  headers: " + json.dumps(shown, indent=4))
        if params:
            print("  query: " + json.dumps(params))
        if body is not None:
            print("  body: " + json.dumps(body, indent=4))
        return None
    wait = MIN_GAP - (time.monotonic() - last_call)
    if wait > 0:
        time.sleep(wait)
    last_call = time.monotonic()
    resp = http.request(method, BASE_URL + path, headers=headers, json=body, params=params, timeout=15)
    if resp.status_code >= 400:
        sys.exit("HTTP " + str(resp.status_code) + " from " + method + " " + path + ": " + resp.text[:300])
    print(method + " " + path + ": HTTP " + str(resp.status_code))
    return resp


def main():
    # GET /port/v1/accounts/me (version v1): Lists every account under the client the logged in user belongs to. Each account carries the AccountKey you pass to price, order and portfolio calls.
    resp = request("GET", "/port/v1/accounts/me")
    account_key = resp.json()["Data"][0]["AccountKey"] if resp is not None else "<Data[0].AccountKey>"
    print("Data[0].AccountKey: " + str(account_key))
    # GET /port/v1/clients/me (version v1): Returns the client the logged in user belongs to, including ClientKey, default account and default currency. You need the ClientKey for some history and performance calls.
    resp = request("GET", "/port/v1/clients/me")
    client_key = resp.json()["ClientKey"] if resp is not None else "<ClientKey>"
    print("ClientKey: " + str(client_key))
    # GET /port/v1/positions/me (version v1): Lists each open position for the logged in client, with opening details in PositionBase and live price and profit or loss in PositionView.
    resp = request("GET", "/port/v1/positions/me")
    if resp is not None:
        print("Open positions: " + json.dumps(resp.json())[:600])
    # POST /trade/v2/orders (version v2): Places a new order, optionally with related limit and stop orders as children. A 200 reply can still carry ErrorInfo, so check for OrderId before treating the order as placed.
    ORDER["AccountKey"] = account_key  # read above
    if SEND and any(v in ("...", 0) for v in walk(ORDER)):
        sys.exit("Set every placeholder in ORDER before a real send.")
    resp = request("POST", "/trade/v2/orders", body=ORDER)
    ref = resp.json()["OrderId"] if resp is not None else "<OrderId from the order reply>"
    print("OrderId: " + str(ref))
    # GET /port/v1/orders/{ClientKey}/{OrderId} (version v1): One open order of a client, looked up by the client's key and the order id.
    resp = request("GET", "/port/v1/orders/" + client_key + "/" + str(ref))
    if resp is not None:
        print("Confirmation: " + json.dumps(resp.json(), indent=2))
    else:
        print("Dry run finished. Nothing was sent. Run again with the word send as the first argument to send these requests to the demo host.")


def walk(value):
    """Every leaf value in a nested dict or list, for the placeholder check."""
    if isinstance(value, dict):
        for v in value.values():
            yield from walk(v)
    elif isinstance(value, list):
        for v in value:
            yield from walk(v)
    else:
        yield value


if __name__ == "__main__":
    try:
        main()
    except requests.RequestException as err:
        sys.exit("Network error: " + str(err))
```

Status: verified. Source: [developer.saxo](https://www.developer.saxo/openapi/referencedocs/trade/v2/orders/post__trade), checked 2026-09-19.

## Sources

- Status: verified. Source: [Saxo OpenAPI Learn (checked 19 Sep 2026)](https://www.developer.saxo/openapi/learn), checked 2026-09-19.
- Status: verified. Source: [Environments, Saxo OpenAPI Learn (checked 19 Sep 2026)](https://www.developer.saxo/openapi/learn/environments), checked 2026-09-19.
- Status: verified. Source: [Security, Saxo OpenAPI Learn (checked 19 Sep 2026)](https://www.developer.saxo/openapi/learn/security), checked 2026-09-19.
- Status: verified. Source: [Users, Saxo OpenAPI reference (checked 19 Sep 2026)](https://www.developer.saxo/openapi/referencedocs/port/v1/users/getuser), checked 2026-09-19.
- Status: verified. Source: [Rate Limiting, Saxo OpenAPI Learn (checked 18 Sep 2026)](https://www.developer.saxo/openapi/learn/rate-limiting), checked 2026-09-19.

## Saxo OpenAPI limits to pace against

- Headline limit: 120/min per session, per service group
  - Scope: session
  - Status: verified. Source: [developer.saxo](https://www.developer.saxo/openapi/learn/rate-limiting), checked 2026-09-18.
  - Note: Per session per service group; 1 order per second per session.

## Next on the Saxo API

- [Saxo API error codes](https://openbrokerapi.com/errors/saxo): 143 decoded, each with what it means, and the fix where the source gives one.
- [Saxo API endpoints](https://openbrokerapi.com/brokers/saxo/endpoints): 68 documented, each with its method, path, parameters and source.
- [Saxo messages](https://openbrokerapi.com/brokers/saxo/messages): 5 documented, each with its fields and source.
- [Saxo API page](https://openbrokerapi.com/brokers/saxo): The full record: access, authentication, limits and the source for each.

## Licence and how to cite

The records behind this page are all rights reserved. This file is the page itself, at its own address, in another format. It is not part of the CC BY 4.0 downloads listed at https://openbrokerapi.com/data. Quote from it with a link to the page.

Cite as: Saxo OpenAPI quickstart: simulation account to first call. Broker API Hub, https://openbrokerapi.com/brokers/saxo/quickstart, newest check 2026-09-19.

Where you rely on a single figure, cite the source and the checked date printed beside it as well. Where this page and a broker's own documentation differ, the documentation wins.
