Websocket Overview
The WebSocket feed needs to be authenticated. It provides real-time market data and updates for orders and trades.
INFOProduction endpoint:
wss://dma-ws.zerocap.com/v2Sandbox endpoint:
wss://sandbox-ws.zerocap.com/v2
Protocol
The WebSocket feed uses a bidirectional protocol that encodes all messages as JSON objects. All messages have a type attribute that can be used to handle the message appropriately. The server sends these types:
type | Meaning |
|---|---|
message | Connection and subscription acknowledgements and the heartbeat pong. The text is in message. |
error | Something was refused. The message carries error_code (an integer) and error_message. |
price | Market data, see Market Data below. |
order | Order updates, see Orders below. |
trade | Fills, see Trade below. |
{
"type": "error",
"error_code": 400,
"error_message": "type error"
}error_code | error_message | Cause |
|---|---|---|
| 401 | Unauthorized | The connection headers are missing, or the API key, signature or timestamp is invalid. The server closes the connection. |
| 408 | request timeout | The connection timestamp is more than 30 seconds away from the server time. The server closes the connection. |
| 400 | invalid message | The message is not JSON text. |
| 400 | type error | type is missing or unknown. |
| 400 | is_subscribe error | is_subscribe is not true or false. |
| 400 | symbols error | symbols is not a list. |
| 400 | symbols: ['ABC/AUD'] error | The listed markets are not available to your account. The subscription is not changed. |
| 400 | ping timeout | The server has not received a message from you for 30 seconds. The server closes the connection. |
Sending Messages
Authentication
To authenticate, the initial connection must contain the same headers as a REST request (api-key, signature and timestamp, see Authentication). After the connection is authenticated there is no further need to send the headers. The server confirms the connection with:
{
"type": "message",
"message": "Successfully connected."
}If authentication fails, you receive one error message (401 Unauthorized, or 408 request timeout) and the connection is closed.
The Python SDK (0.1.36 or later) signs the connection headers and sends the heartbeat for you. Keep the generator returned by create_connection() and pass it to recv(). Without the SDK, any WebSocket library that can set request headers works; the second tab uses the websockets package (version 11 or later).
from zerocap_api import ZerocapWebsocketClient
api_key = 'YOUR_API_KEY'
api_secret = 'YOUR_API_SECRET'
ws = ZerocapWebsocketClient(api_key, api_secret, env='sandbox') # or env='prod'
stream = ws.create_connection() # connects, then sends a ping every 5 seconds in the background
print(ws.recv(stream)) # {'type': 'message', 'message': 'Successfully connected.'}
ws.send({"type": "price", "symbols": ["USDT/AUD"]})
try:
while True:
message = ws.recv(stream) # one decoded JSON message
print(message)
finally:
ws.close()import hashlib
import hmac
import json
import time
from websockets.sync.client import connect
api_key = 'YOUR_API_KEY'
api_secret = 'YOUR_API_SECRET'
timestamp = str(int(time.time()))
signature = hmac.new(api_secret.encode('utf-8'), timestamp.encode('utf-8'), hashlib.sha256).hexdigest()
headers = {"api-key": api_key, "signature": signature, "timestamp": timestamp}
with connect("wss://sandbox-ws.zerocap.com/v2", additional_headers=headers) as ws:
print(json.loads(ws.recv())) # {'type': 'message', 'message': 'Successfully connected.'}
ws.send(json.dumps({"type": "price", "symbols": ["USDT/AUD"]}))
last_sent = time.time()
while True:
message = json.loads(ws.recv()) # the server's pong arrives every 5 seconds, so this never waits long
print(message)
if time.time() - last_sent > 10:
ws.send(json.dumps({"type": "message", "message": "ping"}))
last_sent = time.time()Subscribing
To begin receiving feed messages, you must send a subscribe message to the server indicating which channel and products to receive. You can subscribe to multiple channels but you must send a unique subscription message for each channel. Each subscription is acknowledged:
{
"type": "message",
"message": "price Subscription successful."
}To unsubscribe, send "is_subscribe": false with the channel type. The server answers price Unsubscribed successful. (or order ..., trade ...). Unsubscribing from price stops all price messages.
// Request
{
"type": "price",
"is_subscribe": false
}Channels
Heartbeat
You must send a heartbeat every 30 seconds or you will be disconnected. Any message you send counts; the usual heartbeat is:
// Request
{
"type": "message",
"message": "ping"
}{"type": "ping"} is also accepted. The SDK sends a ping every 5 seconds.
The server does not reply to your ping. Instead it sends every connection a heartbeat of the type message roughly every 5 seconds, whether or not you send anything. It has one parameter:
message- will always have valuepong.
// Server heartbeat, every 5 seconds
{
"type": "message",
"message": "pong"
}If the server has not received a message from you for 30 seconds, it sends a ping timeout error and closes the connection:
{
"type": "error",
"error_code": 400,
"error_message": "ping timeout"
}Market Data
The price channel streams executable pricing for the markets listed in symbols. symbols is always a list, even for one market; use the market names returned by Get Instruments.
// Request
{
"type": "price",
"symbols": ["USDT/AUD"]
}Multiple markets can also be subscribed at the same time. A new price subscription replaces the markets of the previous one, so list every market you want in one message.
// Request
{
"type": "price",
"symbols": ["USDT/AUD", "USDT/USD"]
}The price channel sends a message with the following fields:
timestampis the time of the event as recorded by our trading engine, in Unix seconds with a fractional part (for example1706725399.280178).symbolis the market for which pricing is being streamed.exchangeis the source of the pricing.bidsandasksare pricing for selling and buying. Each level is arranged with the best price as the first item. Within each item the first value is the price, and the second value is the quantity for which the price is valid.
INFOThe returned pricing is not an order book. Placing an order greater than one or more of the levels will fill at the appropriate level, instead of sweeping each level in order. For example, for the below response, placing a sell order for 300,000 USDT at price 1.5136 will fill the entire order at 1.5136, instead of filling 250,000 at 1.5137 and the remainder at 1.5136.
// Response
{
"type":"price",
"data":{
"timestamp":1706725399.280178,
"symbol":"USDT/AUD",
"exchange":"zerocap",
"bids":[
[
1.5137,
250000
],
[
1.5136,
500000
]
],
"asks":[
[
1.5192,
250000
],
[
1.5193,
500000
]
]
}
}Orders
The order channel streams order updates. An order can produce more than one update (for example open, then closed); the last one carries the final status.
// Request
{
"type": "order"
}The order channel sends a message with the following fields:
timestampis the time of the event as recorded by our trading engine, in Unix milliseconds (an integer).last_trade_timestampis the Unix time in milliseconds of the most recent trade on this order.idis the order ID.symbolis the market for which the order was placed.statusis the current status of the order. The available statuses are 'open', 'closed', 'canceled', and 'rejected'.typeis the type of order (alwayslimit).time_in_forceindicates how long an order will remain active before it is executed or expires. It is alwaysFOK(fill-or-kill): the order is filled immediately in full, or not at all.sideis the direction of the order (buyorsell).priceis the limit price of the order. It is distinct from the price the order was filled at.averageis the average price the order was filled at.amountis the size of the base asset the order was placed for.filledis the amount of base asset that was executed.remainingis the amount of base asset that has not been executed.costis the amount of quote asset the order was filled for (filled×average). It will be zero if the order was not filled.error_messageis the reason the order was canceled or rejected. It is empty otherwise.tradesis a list of the fills for this order. Each item has the same fields as thedataof a trade message. The list is empty ([]) while the order has no fills.feeis the fee charged for an order, as a string.client_order_idis an optional field for the user to give placed orders a unique identifier.
// Response
{
"type":"order",
"data":{
"id":"f3a1fdab-6aa0-4ea9-ad37-0d9ac62d3090",
"timestamp":1706721454758,
"last_trade_timestamp":1706721454758,
"status":"closed",
"symbol":"USDT/AUD",
"type":"limit",
"time_in_force":"FOK",
"side":"sell",
"price":1.4977,
"average":1.5129,
"amount":100.0,
"filled":100.0,
"remaining":0.0,
"cost":151.29,
"error_message":"",
"fee":"0",
"trades":[
{
"id":"73a8e6c0-7205-4953-b2a5-aabac01216f3",
"timestamp":1706721454758,
"symbol":"USDT/AUD",
"order":"f3a1fdab-6aa0-4ea9-ad37-0d9ac62d3090",
"type":"limit",
"side":"sell",
"taker_or_maker":"taker",
"price":1.5129,
"amount":100.0,
"cost":151.29,
"fee":"0",
"client_order_id":"79de03ab-c2ec-4dd5-8bce-29cbe1b70fca"
}
],
"client_order_id":"79de03ab-c2ec-4dd5-8bce-29cbe1b70fca"
}
}Trade
The trade channel streams fills.
// Request
{
"type": "trade"
}The trade channel sends a message with the following fields:
timestampis the time of the fill as recorded by our trading engine, in Unix milliseconds (an integer).idis the trade ID.orderis the order ID associated with the fill.symbolis the market for which the order was placed.typeis the type of order.sideis the direction of the order.priceis the price of this fill.amountis the base-asset quantity of this fill.costis the quote-asset amount of this fill (amount×price).taker_or_makerindicates whether the order was filled as an aggressor.feeis the fee charged for this fill, as a string.client_order_idis theclient_order_idof the order, if one was given.
Orders are fill-or-kill, so a filled order normally has a single fill whose price and amount match the order's average and filled.
// Response
{
"type":"trade",
"data":{
"id":"73a8e6c0-7205-4953-b2a5-aabac01216f3",
"timestamp":1706721454758,
"symbol":"USDT/AUD",
"order":"f3a1fdab-6aa0-4ea9-ad37-0d9ac62d3090",
"type":"limit",
"side":"sell",
"taker_or_maker":"taker",
"price":1.5129,
"amount":100.0,
"cost":151.29,
"fee":"0",
"client_order_id":"79de03ab-c2ec-4dd5-8bce-29cbe1b70fca"
}
}Updated 23 days ago