A practice trading API you can call right now
19 addresses that answer a real HTTP request with JSON in the shape a trading API uses: an account, positions, a trade history in pages, orders, prices and the errors you will meet. No account, no key, nothing to install.
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.
Everything here is practice data. The account, the instruments and every price are invented, and the paths are this site's own. It is not any broker's API, it is not connected to any market, and it cannot place a trade. No address here asks for a key, a username or a password, and you should never send one.
Your first request in two minutes
- Open a terminal. On Windows that is PowerShell or Command Prompt, on a Mac it is Terminal.
- Type this line and press Enter. On Windows PowerShell, type
curl.exein place ofcurl.curl https://openbrokerapi.com/mock/v1/account.json
- Read what comes back. It is JSON: names and values. Find
balance. Findpractice, which istruein every reply from this API, so you can always tell practice data from the real thing. - That was an HTTP request. Your terminal sent the word GET and an address, and a server sent back a status of 200, which means it worked, and a body, which is the JSON you are reading.
- Now try another address from the list below, or do the same from Python in the lesson on your first HTTP request.
No terminal to hand? Press a Try it button below and your browser makes the same request.
Every address
The base address is https://openbrokerapi.com/mock/v1. Every address answers GET and nothing else. Each file opens in a browser tab too: follow the path.
Account
Account summary
One invented account: its balance, running profit or loss, equity, margin in use and what is available.
curl
curl https://openbrokerapi.com/mock/v1/account.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/account.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))Open positions
Two invented open positions, one BUY and one SELL, each with its size, opening level, current bid and offer, and running profit or loss.
curl
curl https://openbrokerapi.com/mock/v1/positions.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/positions.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))Trade history
A history too long for one reply arrives in pages. Here each page is a file of its own, and its paging.next field holds the path of the page after it, or null on the last one. Start at page 1 and follow next until it is null.
Closed trades, page 1
GET /mock/v1/trades/page-1.json
The five most recent of twelve invented closed trades, newest first, with a paging note. Its next field holds the path of page 2.
curl
curl https://openbrokerapi.com/mock/v1/trades/page-1.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/trades/page-1.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))Closed trades, page 2
GET /mock/v1/trades/page-2.json
The next five closed trades. Its next field holds the path of page 3.
curl
curl https://openbrokerapi.com/mock/v1/trades/page-2.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/trades/page-2.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))Closed trades, page 3
GET /mock/v1/trades/page-3.json
The last two closed trades. Its next field is null, which is how a program knows to stop asking.
curl
curl https://openbrokerapi.com/mock/v1/trades/page-3.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/trades/page-3.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))Orders
Open orders
Two invented working orders that have not been filled: a limit order and a stop order.
curl
curl https://openbrokerapi.com/mock/v1/orders.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/orders.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))An order confirmation: accepted
GET /mock/v1/confirms/my-order-0001.json
What a program reads back after sending an order, here with a status of ACCEPTED and the order's id. No order was sent and reading it changes nothing.
curl
curl https://openbrokerapi.com/mock/v1/confirms/my-order-0001.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/confirms/my-order-0001.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))An order confirmation: rejected
GET /mock/v1/confirms/my-order-0002.json
A confirmation with a status of REJECTED and the reason in its body. No order was sent and reading it changes nothing.
curl
curl https://openbrokerapi.com/mock/v1/confirms/my-order-0002.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/confirms/my-order-0002.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))Market data
Instrument search
The four invented instruments. A real search filters by what you ask for. This address cannot, so it always returns all four.
curl
curl https://openbrokerapi.com/mock/v1/instruments.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/instruments.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))Price snapshot
GET /mock/v1/prices/HBR100.json
One bid and offer for the invented index HBR100, with the day's high, low and change. It never moves.
curl
curl https://openbrokerapi.com/mock/v1/prices/HBR100.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/prices/HBR100.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))Price history
GET /mock/v1/history/HBR100.json
Ten invented daily candles for HBR100, oldest first: open, high, low and close.
curl
curl https://openbrokerapi.com/mock/v1/history/HBR100.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/history/HBR100.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))Price snapshot, second instrument
One bid and offer for the invented share LNTN, with the day's high, low and change. It never moves.
curl
curl https://openbrokerapi.com/mock/v1/prices/LNTN.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/prices/LNTN.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))Price history, second instrument
GET /mock/v1/history/LNTN.json
Ten invented daily candles for LNTN, oldest first, on the same ten dates as HBR100.
curl
curl https://openbrokerapi.com/mock/v1/history/LNTN.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/history/LNTN.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))Price snapshot, third instrument
One bid and offer for the invented commodity TIDE, with the day's high, low and change. It never moves.
curl
curl https://openbrokerapi.com/mock/v1/prices/TIDE.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/prices/TIDE.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))Price history, third instrument
GET /mock/v1/history/TIDE.json
Ten invented daily candles for TIDE, oldest first, on the same ten dates as HBR100.
curl
curl https://openbrokerapi.com/mock/v1/history/TIDE.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/history/TIDE.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))Example errors
These 4 are bodies to read and to write code against. Each is served with status 200, because a host of fixed files cannot choose a status for each request. The status a real server would send is in the body, in error.status.
An expired session
GET /mock/v1/errors/session-expired.jsonServed as 200. The body says 401
The body a server sends when a session has run out and you must sign in again.
curl
curl https://openbrokerapi.com/mock/v1/errors/session-expired.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/errors/session-expired.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))A rate limit refusal
GET /mock/v1/errors/rate-limited.jsonServed as 200. The body says 429
The body a server sends when you have asked too often, with a retryAfterSeconds field and the Retry-After header it would carry.
curl
curl https://openbrokerapi.com/mock/v1/errors/rate-limited.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/errors/rate-limited.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))A rejected order
GET /mock/v1/errors/order-rejected.jsonServed as 200. The body says 422
The body a server sends when it understood an order and refused it, here for lack of funds.
curl
curl https://openbrokerapi.com/mock/v1/errors/order-rejected.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/errors/order-rejected.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))A validation error
GET /mock/v1/errors/validation.jsonServed as 200. The body says 400
The body a server sends when a request is malformed, naming each field to correct.
curl
curl https://openbrokerapi.com/mock/v1/errors/validation.json
Python
import json
import urllib.request
with urllib.request.urlopen("https://openbrokerapi.com/mock/v1/errors/validation.json", timeout=10) as reply:
print(reply.status)
data = json.load(reply)
print(json.dumps(data, indent=2))What it cannot do, and why
The practice API is a folder of files. There is no program behind it. That keeps it free, fast and safe to hammer while you learn, and it sets hard limits that a real API does not have.
- It answers GET only. A request that would change something, such as placing an order, has nowhere to go. Those calls are shown below as examples to read.
- Every address answers with status 200. The 4 example errors carry the status a real server would send inside the body.
- It never changes. The price is the same every time, the positions never close, and the dates in the files are fixed.
- It cannot filter or sort, and it cannot read a page number. Anything after a question mark in the address is ignored, so the instrument search always returns every instrument. The trade history is in pages only because each page is a separate file that names the next.
- Not every instrument has a price and a history. QUILL is listed as closed and has neither, so asking for its price is a way to see what a missing address does: the host answers with status 404 and no JSON.
- The two order confirmations are files about orders that were never sent. Reading one does not place, change or cancel anything.
- There is nothing to sign in to. A real API makes you prove who you are first. The lessons cover that separately, on a demo account.
- It will not tell you how any broker behaves. For that, read the broker's own reference and the records on this site, each with its source.
Calls that change something: examples to read
None of these can be sent. They are pairs: what a program would send, and what a server would send back. They use the same invented account as the files above.
Place an order
POST /mock/v1/ordersExample only. Not a live address
The clientReference is a name you choose for the order. Send the same one twice and a careful server places one order, not two.
The request body
{
"instrumentId": "TIDE",
"direction": "BUY",
"type": "LIMIT",
"size": 3,
"level": 61.5,
"stopLevel": 59,
"limitLevel": 66,
"clientReference": "my-order-0001"
}The reply a server would send, with status 201
{
"practice": true,
"order": {
"orderId": "O-2001",
"clientReference": "my-order-0001",
"instrumentId": "TIDE",
"direction": "BUY",
"type": "LIMIT",
"size": 3,
"level": 61.5,
"status": "WORKING",
"createdAt": "2026-01-02T16:10:00Z"
}
}Cancel an open order
DELETE /mock/v1/orders/O-2001Example only. Not a live address
A cancel carries no body. The order is named in the path.
The request body
No body.
The reply a server would send, with status 200
{
"practice": true,
"order": {
"orderId": "O-2001",
"status": "CANCELLED",
"cancelledAt": "2026-01-05T09:31:00Z"
}
}Close a position
POST /mock/v1/positions/P-1001/closeExample only. Not a live address
Closing a BUY sells at the bid, which is why the close level is the bid from the positions file.
The request body
{
"size": 2
}The reply a server would send, with status 200
{
"practice": true,
"position": {
"positionId": "P-1001",
"status": "CLOSED",
"size": 2,
"closeLevel": 5057,
"profitLoss": 74,
"closedAt": "2026-01-05T09:32:00Z"
}
}Using it from your own code
Call it from a script, a notebook or a web page. The files are served with a header that lets a page on any other site read them, so a request from JavaScript in a browser works. Please pause between calls as you would with a real API: it is a good habit, and the practice costs nothing.
To load every address into an API tool such as Postman, Insomnia, Bruno or Swagger UI, import /mock/v1/openapi.json. It is an OpenAPI 3.1 file made from the same list of files as this page, with each file as the example reply. Like the files, it is invented practice data and describes no broker's API.
Check practice in the reply before you trust a number. Code that has only ever run against this API has not been tested against a broker. Move to a broker's demo account next, never straight to a live one.