# Capital.com API quickstart: key 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/capital-com/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.

Capital.com API quickstart: 2FA, API key, session, GET /accounts and a complete Python script for the demo host that keeps the session alive. Placeholder credentials only.

- Checked: 2026-09-18

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. Turn on 2FA first

Two factor authentication must be enabled before the platform will let you generate a key. A demo account can be used.

Status: verified. Source: [open-api.capital.com](https://open-api.capital.com/), checked 2026-09-18.

### 2. Generate the key

Settings, then API integrations, then Generate new key. You set a label, a custom password for the key, an optional expiry date, and confirm with your 2FA code.

Status: verified. Source: [open-api.capital.com](https://open-api.capital.com/), checked 2026-09-18.

### 3. Create a session

POST /api/v1/session with X-CAP-API-KEY, your login as identifier and the key's custom password as password. CST and X-SECURITY-TOKEN come back in the headers. Limited to 1 request per second per key.

Python:

```python
# Set CAPITAL_API_KEY, CAPITAL_IDENTIFIER, CAPITAL_PASSWORD as environment variables first. Never paste real values into code or into this site.
import os
import requests

BASE_URL = "https://demo-api-capital.backend-capital.com"
headers = {
    "X-CAP-API-KEY": os.environ["CAPITAL_API_KEY"],
    "Content-Type": "application/json",
}
# CAPITAL_PASSWORD is the custom password you set for the API key
payload = {"identifier": os.environ["CAPITAL_IDENTIFIER"], "password": os.environ["CAPITAL_PASSWORD"]}

resp = requests.post(BASE_URL + "/api/v1/session", headers=headers, json=payload, timeout=10)
resp.raise_for_status()
print("CST:", resp.headers.get("CST") is not None)
print("X-SECURITY-TOKEN:", resp.headers.get("X-SECURITY-TOKEN") is not None)
# Keep both header values for the next call, for example as CAPITAL_CST and CAPITAL_SECURITY_TOKEN.
# Never commit them to source control.
```

JavaScript:

```javascript
// Set CAPITAL_API_KEY, CAPITAL_IDENTIFIER, CAPITAL_PASSWORD as environment variables first. Never paste real values into code or into this site.
// Node 18 or later. Save as a .mjs file so top level await works.
const BASE_URL = "https://demo-api-capital.backend-capital.com";

const res = await fetch(`${BASE_URL}/api/v1/session`, {
  method: "POST",
  headers: {
    "X-CAP-API-KEY": process.env.CAPITAL_API_KEY,
    "Content-Type": "application/json"
  },
  // CAPITAL_PASSWORD is the custom password you set for the API key
  body: JSON.stringify({ identifier: process.env.CAPITAL_IDENTIFIER, password: process.env.CAPITAL_PASSWORD })
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const cst = res.headers.get("CST");
const securityToken = res.headers.get("X-SECURITY-TOKEN");
console.log("Session tokens received:", Boolean(cst && securityToken));
```

curl:

```bash
# Set CAPITAL_API_KEY, CAPITAL_IDENTIFIER, CAPITAL_PASSWORD as environment variables first. Never paste real values into code or into this site.
curl -i -X POST "https://demo-api-capital.backend-capital.com/api/v1/session" \
  -H "X-CAP-API-KEY: $CAPITAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"identifier\":\"$CAPITAL_IDENTIFIER\",\"password\":\"$CAPITAL_PASSWORD\"}"
```

Status: verified. Source: [open-api.capital.com](https://open-api.capital.com/), checked 2026-09-18.

### 4. First call: accounts

GET /api/v1/accounts with CST and X-SECURITY-TOKEN.

Python:

```python
# Set CAPITAL_CST, CAPITAL_SECURITY_TOKEN as environment variables first. Never paste real values into code or into this site.
import os
import requests

BASE_URL = "https://demo-api-capital.backend-capital.com"
headers = {
    "CST": os.environ["CAPITAL_CST"],
    "X-SECURITY-TOKEN": os.environ["CAPITAL_SECURITY_TOKEN"],
}

resp = requests.get(BASE_URL + "/api/v1/accounts", headers=headers, timeout=10)
resp.raise_for_status()
print(resp.json())
```

JavaScript:

```javascript
// Set CAPITAL_CST, CAPITAL_SECURITY_TOKEN as environment variables first. Never paste real values into code or into this site.
// Node 18 or later. Save as a .mjs file so top level await works.
const BASE_URL = "https://demo-api-capital.backend-capital.com";

const res = await fetch(`${BASE_URL}/api/v1/accounts`, {
  method: "GET",
  headers: {
    "CST": process.env.CAPITAL_CST,
    "X-SECURITY-TOKEN": process.env.CAPITAL_SECURITY_TOKEN
  }
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
console.log(await res.json());
```

curl:

```bash
# Set CAPITAL_CST, CAPITAL_SECURITY_TOKEN as environment variables first. Never paste real values into code or into this site.
curl "https://demo-api-capital.backend-capital.com/api/v1/accounts" \
  -H "CST: $CAPITAL_CST" \
  -H "X-SECURITY-TOKEN: $CAPITAL_SECURITY_TOKEN"
```

Status: verified. Source: [open-api.capital.com](https://open-api.capital.com/), checked 2026-09-18.

### 5. Keep it alive

A session is active for 10 minutes; after longer without a request, the next call fails. GET /api/v1/ping keeps it alive and answers {"status": "OK"}. The general limit is 10 requests per second per user, POST /api/v1/session allows 1 per second, and going over returns HTTP 429.

Status: verified. Source: [open-api.capital.com](https://open-api.capital.com/), checked 2026-09-18.

### 6. Run the complete script

One Python file that does all of the above on the demo host: reads CAPITAL_API_KEY, CAPITAL_IDENTIFIER and CAPITAL_PASSWORD from environment variables, logs in once, keeps CST and X-SECURITY-TOKEN in memory only, lists your accounts, pings every 5 minutes while idle and backs off on HTTP 429. It refuses to run against any host but demo-api-capital.backend-capital.com. Save it as capital_demo.py and run python3 capital_demo.py.

Python:

```python
#!/usr/bin/env python3
"""Capital.com demo API: log in once, list accounts, keep the session alive.

Unofficial example from Broker API Hub, not a Capital.com product. Demo host only.
Before running, set these environment variables on your own machine:
  CAPITAL_API_KEY      the key from Settings > API integrations
  CAPITAL_IDENTIFIER   your Capital.com login
  CAPITAL_PASSWORD     the custom password you set for that API key
Never paste real values into this file or into any website.
Needs Python 3.8 or later and requests (pip install requests). Stop with Ctrl+C.
"""
import json
import os
import sys
import time

import requests

DEMO_HOST = "https://demo-api-capital.backend-capital.com"
BASE_URL = os.environ.get("CAPITAL_BASE_URL", DEMO_HOST).rstrip("/")
PING_EVERY = 5 * 60   # seconds; the session lapses after 10 minutes without requests
MAX_TRIES = 5         # attempts per request while the API answers HTTP 429

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


def env(name):
    value = os.environ.get(name)
    if not value:
        sys.exit("Set the " + name + " environment variable first.")
    return value


# CST and X-SECURITY-TOKEN live in this object's memory only: never printed, never saved.
http = requests.Session()
http.headers["Accept"] = "application/json"


def call(method, path, **kwargs):
    """One request, backing off on HTTP 429 (1s, 2s, 4s and so on, up to 60s)."""
    delay = 1
    for _ in range(MAX_TRIES):
        resp = http.request(method, BASE_URL + path, timeout=15, **kwargs)
        if resp.status_code != 429:
            resp.raise_for_status()
            return resp
        wait = delay
        retry_after = resp.headers.get("Retry-After", "")
        if retry_after.isdigit():
            wait = max(wait, int(retry_after))
        print("HTTP 429 on " + path + ", waiting " + str(wait) + "s")
        time.sleep(wait)
        delay = min(delay * 2, 60)
    raise RuntimeError("Still rate limited on " + path + " after " + str(MAX_TRIES) + " tries")


def log_in():
    # POST /session allows 1 request per second, so this script logs in exactly once.
    resp = call(
        "POST",
        "/api/v1/session",
        headers={"X-CAP-API-KEY": env("CAPITAL_API_KEY"), "Content-Type": "application/json"},
        # encryptedPassword is optional; leaving it out sends the plain custom password.
        json={"identifier": env("CAPITAL_IDENTIFIER"), "password": env("CAPITAL_PASSWORD")},
    )
    cst, token = resp.headers.get("CST"), resp.headers.get("X-SECURITY-TOKEN")
    if not cst or not token:
        sys.exit("Logged in, but CST or X-SECURITY-TOKEN was missing from the response headers.")
    http.headers.update({"CST": cst, "X-SECURITY-TOKEN": token})
    print("Logged in to demo. Session tokens are held in memory only.")


def main():
    try:
        log_in()
        accounts = call("GET", "/api/v1/accounts").json()
        print(json.dumps(accounts, indent=2))
        print("Idle. Pinging every " + str(PING_EVERY // 60) + " minutes to keep the session alive. Ctrl+C to stop.")
        while True:
            time.sleep(PING_EVERY)
            status = call("GET", "/api/v1/ping").json().get("status")
            print(time.strftime("%H:%M:%S") + " ping " + str(status))
    except requests.HTTPError as err:
        code = err.response.status_code if err.response is not None else "?"
        sys.exit("HTTP " + str(code) + " from " + err.request.url.replace(BASE_URL, "")
                 + ". A 401 at login means check the key, login and the key's custom password;"
                 + " a 401 later means the session lapsed, so run the script again.")
    except KeyboardInterrupt:
        print("\nStopped. Session tokens discarded with the process.")


if __name__ == "__main__":
    main()
```

Status: verified. Source: [open-api.capital.com](https://open-api.capital.com/), checked 2026-09-18.

### 7. From first call to first order

One Python file built from the Capital.com endpoint records on this site, as the last step of this quickstart. POST /api/v1/session (version v1) logs in with CAPITAL_API_KEY, CAPITAL_IDENTIFIER and CAPITAL_PASSWORD and keeps CST and X-SECURITY-TOKEN in memory. GET /api/v1/positions (version v1) reads your open positions. POST /api/v1/positions (version v1) places one small demo order with every field the record marks required (direction, epic and size). GET /api/v1/confirms/{dealReference} (version v1) reads the confirmation or status. It sends at most one request every second, which stays under each request limit in the Capital.com limits table below. It refuses to run against any host but https://demo-api-capital.backend-capital.com, and it never prints a token. Dry run is the default: save it as capital_first_order.py, run python capital_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 capital_first_order.py send to send them to the demo host. The script has not been run against Capital.com.

Python:

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

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

What it does, in order:
  1. POST /api/v1/session (version v1): logs in and keeps CST and X-SECURITY-TOKEN in memory
  2. GET /api/v1/positions (version v1): reads your open positions
  3. POST /api/v1/positions (version v1): places one small demo order with every field the record marks required
  4. GET /api/v1/confirms/{dealReference} (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 capital_first_order.py send) to send the requests to the demo host.

Before a real send, set these environment variables on your own machine:
  CAPITAL_API_KEY      the key from Settings, then API integrations
  CAPITAL_IDENTIFIER   your Capital.com login
  CAPITAL_PASSWORD     the custom password you set for that API key
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 requests

DEMO_HOST = "https://demo-api-capital.backend-capital.com"
BASE_URL = os.environ.get("CAPITAL_BASE_URL", DEMO_HOST).rstrip("/")
SEND = len(sys.argv) > 1 and sys.argv[1] == "send"
# Pacing, from this site's limits register for Capital.com:
#   10/s per user (user; 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 = {
    "direction": "BUY",  # BUY or SELL. (required)
    "epic": "...",       # Instrument epic, for example from GET /api/v1/markets. (required)
    "size": 0,           # Deal size. (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({
    "X-CAP-API-KEY": env("CAPITAL_API_KEY"),
    "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 log_in():
    """POST /api/v1/session (version v1). Log in with your API key, login identifier and API key password. The CST and X-SECURITY-TOKEN tokens come back in the response headers."""
    resp = request("POST", "/api/v1/session", version="v1",
                   body={"identifier": env("CAPITAL_IDENTIFIER"), "password": env("CAPITAL_PASSWORD")})
    if resp is None:
        http.headers.update({"CST": "<CAPITAL_CST>", "X-SECURITY-TOKEN": "<CAPITAL_SECURITY_TOKEN>"})
        return
    capital_cst = resp.headers.get("CST")
    capital_security_token = resp.headers.get("X-SECURITY-TOKEN")
    if not (capital_cst and capital_security_token):
        sys.exit("Logged in, but CST or X-SECURITY-TOKEN was missing from the response headers.")
    http.headers.update({"CST": capital_cst, "X-SECURITY-TOKEN": capital_security_token})
    print("Logged in to demo. CST and X-SECURITY-TOKEN held in memory only.")


def main():
    log_in()
    # GET /api/v1/positions (version v1): All open positions on the active account, each with position and market details.
    resp = request("GET", "/api/v1/positions")
    if resp is not None:
        print("Open positions: " + json.dumps(resp.json())[:600])
    # POST /api/v1/positions (version v1): Open a position at market. The response is only a dealReference: confirm the result with GET /api/v1/confirms/{dealReference}.
    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/v1/positions", body=ORDER)
    ref = resp.json()["dealReference"] if resp is not None else "<dealReference from the order reply>"
    print("dealReference: " + str(ref))
    # GET /api/v1/confirms/{dealReference} (version v1): Check whether a deal was accepted or rejected. For a new position, the opened deal IDs are in affectedDeals.
    resp = request("GET", "/api/v1/confirms/" + 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: [open-api.capital.com](https://open-api.capital.com/), checked 2026-09-18.

## Sources

- Status: verified. Source: [Capital.com Public API (checked 18 Sep 2026)](https://open-api.capital.com/), checked 2026-09-18.
- Status: verified. Source: [GitHub: capital-com-sv/capital-api-postman (demo environment file, checked 18 Sep 2026)](https://github.com/capital-com-sv/capital-api-postman), checked 2026-09-18.

## Capital.com limits to pace against

- Headline limit: 10/s per user
  - Scope: user
  - Status: verified. Source: [open-api.capital.com](https://open-api.capital.com/), checked 2026-09-18.
  - Note: Orders and new positions: 1 per 0.1 s. POST /session: 1 a second per API key. Demo: 1000 an hour for POST /positions and /workingorders.

## Next on the Capital.com API

- [Capital.com API error codes](https://openbrokerapi.com/errors/capital-com): 24 decoded, each with what it means, and the fix where the source gives one.
- [Capital.com API endpoints](https://openbrokerapi.com/brokers/capital-com/endpoints): 36 documented, each with its method, path, parameters and source.
- [Capital.com messages](https://openbrokerapi.com/brokers/capital-com/messages): 7 documented, each with its fields and source.
- [Capital.com API page](https://openbrokerapi.com/brokers/capital-com): The full record: access, authentication, limits and the source for each.
- [Get your Capital.com trade history into a spreadsheet](https://openbrokerapi.com/guides/capital-com-trade-history): Guide
- [Capital.com API with Python](https://openbrokerapi.com/guides/capital-com-api-python): Guide

## 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: Capital.com API quickstart: key to first call. Broker API Hub, https://openbrokerapi.com/brokers/capital-com/quickstart, newest check 2026-09-18.

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.
