Private WebSocket
Trading on margin can lose you more than you put in. Past results, including backtests, do not predict future results. Cloud9Trader provides tools and education, not financial advice.
The private socket is the public socket plus your own account: balances, positions, orders and executions from every broker you have connected, and order submission. It uses the same address, the same messages and the same public topics.
Orders sent here go to your real broker account. Connect a practice account first: see Connect an OANDA practice account. Your demo account’s balances, positions, orders and executions stream here too, tagged with the broker Demo, but it takes no orders through the API.
Sign the connection
Section titled “Sign the connection”You need an API key and its secret, from API Access in your account settings. See Authentication and keys.
Send three headers with the connection request:
| Header | Value |
|---|---|
x-c9t-key |
Your API key |
x-c9t-nonce |
The current time in milliseconds since 1 January 1970, as an integer |
x-c9t-signature |
HMAC-SHA256 of "/" followed by the nonce, keyed with your secret decoded from base64, as lowercase hex |
The server refuses a connection when:
- the nonce is more than 2 seconds from its clock, so keep your clock in sync;
- the nonce is not larger than the last one used with that key in the last 5 seconds, so never reuse one;
- the request has an
OriginorRefererheader. A web browser always sends one, so a private socket can only be opened from a server or script, where the secret stays safe.
import { createHmac } from "node:crypto"import WebSocket from "ws"
const key = process.env.C9T_API_KEY!const secret = process.env.C9T_API_SECRET!
const nonce = Date.now().toString()const signature = createHmac("sha256", Buffer.from(secret, "base64")).update("/" + nonce).digest("hex")
const socket = new WebSocket("wss://sockets.cloud9trader.com/", { headers: { "x-c9t-key": key, "x-c9t-nonce": nonce, "x-c9t-signature": signature },})
socket.on("open", () => socket.send(JSON.stringify(["subscribe", "positions"])))socket.on("message", (raw) => console.log(JSON.parse(raw.toString())))socket.on("unexpected-response", (_request, response) => console.error("Refused:", response.statusCode))import asyncio, base64, hashlib, hmac, json, os, time, websockets
key = os.environ["C9T_API_KEY"]secret = os.environ["C9T_API_SECRET"]
async def main(): nonce = str(int(time.time() * 1000)) signature = hmac.new(base64.b64decode(secret), ("/" + nonce).encode(), hashlib.sha256).hexdigest() headers = {"x-c9t-key": key, "x-c9t-nonce": nonce, "x-c9t-signature": signature} async with websockets.connect("wss://sockets.cloud9trader.com/", additional_headers=headers) as socket: await socket.send(json.dumps(["subscribe", "positions"])) async for raw in socket: print(json.loads(raw))
asyncio.run(main())Make a new nonce and signature for every connection, including every reconnect.
When the connection is refused
Section titled “When the connection is refused”The server answers the connection request with an HTTP status instead of opening the socket:
| Status | Message | Fix |
|---|---|---|
401 |
'x-c9t-signature' is a required header for HMAC authentication |
Send the signature header. |
401 |
'x-c9t-nonce' is a required header for HMAC authentication |
Send the nonce header. |
401 |
'x-c9t-nonce' value must be an integer |
Send milliseconds as a whole number. |
401 |
'x-c9t-nonce' value must be current time in milliseconds elapsed since January 1, 1970. Ensure your system clock is accurate |
Sync your clock. |
401 |
'x-c9t-nonce' value was too small |
Use a new, larger nonce. |
401 |
Key is incorrect |
Check the key, or create a new one. |
401 |
Key or signature are incorrect |
Check the secret and how you sign. |
403 |
Authenticated connections must not be made from web browsers |
Remove the Origin and Referer headers, and connect from a server. |
500 |
Server error, Please retry |
Try again shortly. |
Private topics
Section titled “Private topics”| Topic | Request | Subscribe | Data |
|---|---|---|---|
balances |
Yes | Yes | Each account’s balance and margin |
positions |
Yes | Yes | Open positions |
orders |
Yes | Yes | Working orders and their changes |
executions |
Yes | Yes | Fills |
messages |
No | Automatic | Notices from your broker connections |
logs |
No | Automatic | Log lines about your connections |
A request, or the first message of a subscription, is a snapshot: an array of items from every connected broker, each with snapshot: true. A third element says which brokers answered:
["positions", [{ "broker": "OANDA", "id": "1234", "instrumentId": "EUR_USD:OANDA", "symbol": "EUR_USD", "quantity": 1000, "direction": "LONG", "status": "OPEN", "snapshot": true }], { "connected": ["OANDA"], "disconnected": [] }]After that, updates arrive as [topic, items], each item with its broker. With several brokers, the snapshot arrives as one message per broker.
messages and logs start by themselves when you connect. Private topics cannot be unsubscribed; close the socket to stop them.
Fields
Section titled “Fields”Every item has broker. The other fields depend on the broker; these are the ones to rely on.
| Topic | Fields |
|---|---|
balances |
broker, accountNo, balance, equity, available, used, marginRatio, marginCall |
positions |
broker, id, accountNo, instrumentId, symbol, quantity, direction (LONG or SHORT), status (OPEN or CLOSED), openPrice, profit |
orders |
broker, id, instrumentId, symbol, side (BUY or SELL), type, quantity, quantityFilled, quantityRemaining, price, stopPrice, timeInForce, status (NEW, PARTIALLY_FILLED, FILLED, CANCELED, REJECTED or EXPIRED) |
executions |
broker, id, instrumentId, symbol, time, side, quantity, price, orderId, orderType, orderStatus, fee, feeCurrency |
Submit an order
Section titled “Submit an order”["submit", "order", { "broker": "Bitfinex", "symbol": "BTCUSD", "side": "BUY", "type": "LIMIT", "quantity": 0.01, "limitPrice": 60000 }, "my-order-1"]| Field | Required | Values |
|---|---|---|
broker |
Yes | A broker you have connected, as it appears in balances, such as OANDA or Bitfinex |
symbol |
Yes | The broker’s symbol, 3 to 20 characters, such as EUR_USD or BTCUSD |
side |
Yes | BUY or SELL |
type |
Yes | MARKET, LIMIT, STOP or STOP_LIMIT |
quantity |
Yes | A number, in units of the instrument |
limitPrice |
For LIMIT and STOP_LIMIT |
A number |
stopPrice |
For STOP and STOP_LIMIT |
A number |
timeInForce |
No | GTC, DAY, IOC, FOK or OPG |
What each broker takes through the API:
| Broker | Order types | Time in force |
|---|---|---|
| OANDA | MARKET only. Other types are refused with “Only market orders are currently supported”. |
FOK or IOC |
| Bitfinex | MARKET, LIMIT, STOP and STOP_LIMIT, on the exchange wallet |
As the exchange allows |
With a requestId, the answer is ["my-order-1", null, order] when the broker accepts it (OANDA sends no order), or ["my-order-1", "reason"] when it does not. Without one, success arrives as ["message", "Order submitted successfully"], failure as ["error", reason], and the order itself on orders. Always send a requestId, so you know which order an answer is about.
Close a position
Section titled “Close a position”Closing positions through the API is not working at the moment. The request below is accepted, but on OANDA and Bitfinex no position is closed and no answer comes back. Close positions on your broker’s own site until this is fixed.
["submit", "close-position", { "broker": "OANDA", "brokerPositionId": "1234" }, "close-1"]| Field | Required | Values |
|---|---|---|
broker |
Yes | The position’s broker |
brokerPositionId |
Yes | The position’s id from positions |
When it works, the answer will be ["close-1", null], or ["close-1", "reason"] if the broker refuses.
Rejections
Section titled “Rejections”The reason in an order or close answer is one of these. <field> and <broker> stand for the value concerned; several validation messages arrive together, one per line.
| Message | Meaning |
|---|---|
<field> is a required field |
A required field is missing. |
<field> value not recognized |
A field is not one of its allowed values. |
<field> must be a number |
quantity, limitPrice or stopPrice is not a number. |
<field> must be a string |
A text field is not a string. |
<field> cannot be less than 0 |
A quantity or price is negative. |
Instrument symbol must be at least 3 characters long |
symbol is too short. |
Instrument symbol must be under 20 characters long |
symbol is too long. |
<field> is required for <type> orders |
limitPrice or stopPrice is missing for that order type. |
No connection available for <broker>. Please provide API keys |
You have no live connection to that broker. Add its keys in Exchange API Keys. |
Error submitting order: <reason> |
The broker refused the order or the close. The reason is the broker’s. |
Errors fetching a snapshot from one broker arrive as ["error", "Error fetching <topic> for <broker>"]; the other brokers’ items still arrive.
Read next
Section titled “Read next”- WebSocket API: messages, public topics, keep-alive and reconnection.
- WebSocket client library: does the signing for you in JavaScript.