Broker API HubMenu

Trading 212 API quickstart: practice key to first call

Make a first Trading 212 API call on the paper trading host: a key and secret, the Basic header and GET account summary in Python, JavaScript or curl. Each step is sourced on Trading 212's own pages. The samples were written from the documentation and have not been run against Trading 212.

Independent and unofficialChecked Code not yet run on a demo account

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.

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. What you need before you start

    Trading 212 calls this API a beta and says it is enabled and usable only for the Invest and Stocks ISA account types. Its documentation names two environments, paper trading (demo) and live trading, and this quickstart uses paper trading only. On your own machine you need one of three things: Python 3 with the requests package, Node 18 or later, or curl. You do not need a wrapper library. Nothing on this page asks for your key: the samples read it from environment variables that you set yourself.

    docs.trading212.comchecked

  2. Generate a key and a secret

    Trading 212's help centre gives the route: open the menu, choose Settings, then API (Beta), then Generate API key. A risk warning has to be accepted first. You then review the permissions on offer (the article gives account data, history, orders and portfolio as examples), name the key and choose between access from any IP address and access restricted to addresses you list. You receive an API key and a secret, and the article says the secret is shown only once, so store it where your code can read it and nobody else can. For this quickstart a key that can read account data is enough: leave the order permissions off until you need them. The article says key versions may differ between real and demo accounts. It does not spell out, in the text opened for this page, the steps for making a key on the practice account, so check which account you are in before you generate one.

    helpcentre.trading212.comchecked

  3. Point at the paper trading host

    Paper trading (demo) is https://demo.trading212.com/api/v0 and live trading is https://live.trading212.com/api/v0. The paths are the same on both, so the host is the only thing that separates a practice call from a real one. Every sample on this page names the demo host in full. Do not change it until you have read the checklist for going live, and never as a quick test.

    docs.trading212.comchecked

  4. Build the Authorization header

    Trading 212 uses HTTP Basic authentication. Join the key and the secret with a colon, encode the result in Base64 and send it in the Authorization header after the word Basic. There is no login call and no session: the same header goes on every request. The samples in the next step read the two values from the T212_API_KEY and T212_API_SECRET environment variables and build the header in memory. The curl sample uses the -u option, which makes curl build the same header for you.

    docs.trading212.comchecked

  5. First call: account summary

    GET /api/v0/equity/account/summary is read only and takes no parameters, which makes it a cheap way to prove that the key, the secret and the host agree. Trading 212 documents it at 1 request every 5 seconds, so run it once and read the reply before running it again.

    Python
    # Set T212_API_KEY and T212_API_SECRET as environment variables first. Never paste real values into code or into this site.
    # Written from Trading 212's documentation. Not run against Trading 212.
    import base64
    import os
    import requests
    
    BASE_URL = "https://demo.trading212.com"  # paper trading (demo) host
    pair = os.environ["T212_API_KEY"] + ":" + os.environ["T212_API_SECRET"]
    headers = {
        "Authorization": "Basic " + base64.b64encode(pair.encode()).decode(),
    }
    
    resp = requests.get(BASE_URL + "/api/v0/equity/account/summary", headers=headers, timeout=10)
    print("HTTP", resp.status_code, "requests left in this period:", resp.headers.get("x-ratelimit-remaining"))
    resp.raise_for_status()
    summary = resp.json()
    print(summary["currency"], summary["totalValue"], summary["cash"]["availableToTrade"])
    
    JavaScript
    // Set T212_API_KEY and T212_API_SECRET as environment variables first. Never paste real values into code or into this site.
    // Written from Trading 212's documentation. Not run against Trading 212.
    // Node 18 or later. Save as a .mjs file so top level await works.
    const BASE_URL = "https://demo.trading212.com"; // paper trading (demo) host
    const pair = `${process.env.T212_API_KEY}:${process.env.T212_API_SECRET}`;
    
    const res = await fetch(`${BASE_URL}/api/v0/equity/account/summary`, {
      method: "GET",
      headers: {
        "Authorization": `Basic ${Buffer.from(pair).toString("base64")}`
      }
    });
    console.log("HTTP", res.status, "requests left in this period:", res.headers.get("x-ratelimit-remaining"));
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const summary = await res.json();
    console.log(summary.currency, summary.totalValue, summary.cash.availableToTrade);
    
    curl
    # Set T212_API_KEY and T212_API_SECRET as environment variables first. Never paste real values into code or into this site.
    # Written from Trading 212's documentation. Not run against Trading 212.
    # -u makes curl build the Authorization: Basic header from the key and the secret.
    curl -s -u "$T212_API_KEY:$T212_API_SECRET" \
      "https://demo.trading212.com/api/v0/equity/account/summary"
    

    docs.trading212.comchecked

  6. Read the reply

    A 200 reply is one JSON object. id is the account number and currency is the account currency. totalValue is the whole account. cash holds three numbers: availableToTrade, reservedForOrders and inPies. investments holds four: currentValue, totalCost, unrealizedProfitLoss and realizedProfitLoss. The numbers are JSON numbers, not strings. On a new practice account the investments figures can all be zero, which is a correct reply and not an error.

    docs.trading212.comchecked

  7. A second read: the instrument list

    GET /api/v0/equity/metadata/instruments returns every instrument the account can reach, each with its ticker, name, shortName, isin, type, currencyCode and workingScheduleId. The ticker is the name the order calls expect, so this is where you look one up. Trading 212 says the data is refreshed every 10 minutes and documents the call at 1 request every 50 seconds: fetch the list once, save it to a file and read the file after that. The key needs the metadata permission for this call.

    docs.trading212.comchecked

  8. The limits to respect

    Trading 212 sets a limit for each endpoint and applies it per account, whichever key or IP address the request comes from, so a second key does not buy a second allowance. Every reply carries five headers: x-ratelimit-limit, x-ratelimit-period, x-ratelimit-remaining, x-ratelimit-used and x-ratelimit-reset, the last being the Unix time at which the allowance resets. Trading 212 says requests may be sent in bursts within a period. The simplest pacing is to read x-ratelimit-remaining after each call and wait until x-ratelimit-reset when it reaches zero. This site's limits register holds one Trading 212 row, the account summary example of 1 request every 5 seconds, with the status verified and a check date of 22 September 2026: it is in the table below this list.

    docs.trading212.comchecked

  9. The errors you are likely to meet first

    Trading 212's reference lists four failures for the account summary call. 401 is described as Bad API key: check that both the key and the secret are in the header, joined by a colon before encoding, and that the key belongs to the environment whose host you are calling. 403 is described as a missing account scope: the key was created without the permission this call needs. 408 is a timeout. 429 means the allowance for this endpoint is used up, so wait for the reset time in the headers and do not retry in a loop. This site keeps a page for each of these under /errors/trading-212, linked in the block at the foot of this page.

    docs.trading212.comchecked

  10. What to read next, and one warning about orders

    Positions, pending orders and history are further read only calls, each on this site's Trading 212 endpoint pages with its own limit. Placing an order is a separate step that this quickstart does not take. Before you try it on the practice account, note what Trading 212's reference says about POST /api/v0/equity/orders/market: in this beta the call is not idempotent, so sending the same request more than once may result in duplicate orders, and the sign of quantity sets the direction. A script that retries a failed order request without first reading the pending orders can therefore place the order twice. Stay on the paper trading host while you learn how that behaves.

    docs.trading212.comchecked

  11. From first call to first order

    One Python file built from the Trading 212 endpoint records on this site, as the last step of this quickstart. It reads T212_API_KEY and T212_API_SECRET from the environment. GET /api/v0/equity/positions (version v0) reads your open positions. POST /api/v0/equity/orders/market (version v0) places one small demo order with the fields the vendor's page marks neither required nor optional (ticker, quantity and extendedHours), each tagged in a comment. GET /api/v0/equity/orders/{id} (version v0) reads the confirmation or status. It sends at most one request every 5 seconds, which stays under each request limit in the Trading 212 limits table below. It refuses to run against any host but https://demo.trading212.com, and it never prints a token. Dry run is the default: save it as t212_first_order.py, run python t212_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 t212_first_order.py send to send them to the demo host. The script has not been run against Trading 212.

    Python
    #!/usr/bin/env python3
    """Trading 212 demo API: from first call to first order.
    
    Unofficial example from Broker API Hub, not published by Trading 212. Written from the Trading 212 endpoint records on
    openbrokerapi.com and not run against Trading 212: the quickstart page says so. Demo host only.
    
    What it does, in order:
      1. proves the token with GET /api/v0/equity/account/summary (version v0)
      2. GET /api/v0/equity/positions (version v0): reads your open positions
      3. POST /api/v0/equity/orders/market (version v0): places one small demo order with every field the record marks required
      4. GET /api/v0/equity/orders/{id} (version v0): 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 t212_first_order.py send) to send the requests to the demo host.
    
    Before a real send, set these environment variables on your own machine:
      T212_API_KEY      the API key for the practice account
      T212_API_SECRET   the secret shown once when the key was made
    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 base64
    
    import requests
    
    DEMO_HOST = "https://demo.trading212.com"
    BASE_URL = os.environ.get("T212_BASE_URL", DEMO_HOST).rstrip("/")
    SEND = len(sys.argv) > 1 and sys.argv[1] == "send"
    # Pacing, from this site's limits register for Trading 212:
    #   1 req/5s, account summary (example) (account; checked 12 Oct 2026)
    # One request every 5 seconds stays under each of them.
    MIN_GAP = 5
    
    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 = {
        "ticker": "...",         # Instrument ticker as Trading 212 writes it, for example AAPL_US_EQ. Read it from the instruments call. (not marked by the vendor)
        "quantity": 0,           # Number of shares. Positive to buy, negative to sell. (not marked by the vendor)
        "extendedHours": False,  # True to let the order fill outside the regular session. Default false. (not marked by the vendor)
    }
    
    
    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": "Basic " + (base64.b64encode((env("T212_API_KEY") + ":" + env("T212_API_SECRET")).encode()).decode() if SEND else "<from T212_API_KEY and T212_API_SECRET>"),
        "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
        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 /api/v0/equity/account/summary (version v0): Cash and investment figures for the account: funds free to trade, cash reserved for pending orders, the value and cost of holdings, and the total.
        resp = request("GET", "/api/v0/equity/account/summary")
        account_id = resp.json()["id"] if resp is not None else "<id>"
        print("id: " + str(account_id))
        # GET /api/v0/equity/positions (version v0): Every open position on the account, with quantity, average price paid, current price and the effect on the account in its own currency.
        resp = request("GET", "/api/v0/equity/positions")
        if resp is not None:
            print("Open positions: " + json.dumps(resp.json())[:600])
        # POST /api/v0/equity/orders/market (version v0): Place a market order, to deal at the next available price. The sign of quantity sets the direction. If the market is closed the order waits for it to open.
        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", "/api/v0/equity/orders/market", body=ORDER)
        ref = resp.json()["id"] if resp is not None else "<id from the order reply>"
        print("id: " + str(ref))
        # GET /api/v0/equity/orders/{id} (version v0): One pending order, looked up by its numeric identifier. Use it to check on an order you placed earlier.
        resp = request("GET", "/api/v0/equity/orders/" + 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))
    
    

    docs.trading212.comchecked

Trading 212 limits to pace against

LimitValueScopeSource
Headline limit1 req/5s, account summary (example)accountdocs.trading212.comchecked

Sources

Sections