Capital.com API quickstart: key to first call
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.
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
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.
open-api.capital.comchecked
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.
open-api.capital.comchecked
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
# 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
// 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
# 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\"}"open-api.capital.comchecked
First call: accounts
GET /api/v1/accounts with CST and X-SECURITY-TOKEN.
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
// 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
# 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"
open-api.capital.comchecked
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.
open-api.capital.comchecked
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
#!/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()open-api.capital.comchecked
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
#!/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))open-api.capital.comchecked
Capital.com limits to pace against
| Limit | Value | Scope | Source |
|---|---|---|---|
| Headline limit | 10/s per user | user | open-api.capital.comchecked |