Websocket Overview

The WebSocket feed needs to be authenticated. It provides real-time market data and updates for orders and trades.

🔗

INFO

Production endpoint: wss://dma-ws.zerocap.com/v2

Sandbox 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:

typeMeaning
messageConnection and subscription acknowledgements and the heartbeat pong. The text is in message.
errorSomething was refused. The message carries error_code (an integer) and error_message.
priceMarket data, see Market Data below.
orderOrder updates, see Orders below.
tradeFills, see Trade below.
{
    "type": "error",
    "error_code": 400,
    "error_message": "type error"
}
error_codeerror_messageCause
401UnauthorizedThe connection headers are missing, or the API key, signature or timestamp is invalid. The server closes the connection.
408request timeoutThe connection timestamp is more than 30 seconds away from the server time. The server closes the connection.
400invalid messageThe message is not JSON text.
400type errortype is missing or unknown.
400is_subscribe erroris_subscribe is not true or false.
400symbols errorsymbols is not a list.
400symbols: ['ABC/AUD'] errorThe listed markets are not available to your account. The subscription is not changed.
400ping timeoutThe 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 value pong.
// 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:

  • timestamp is the time of the event as recorded by our trading engine, in Unix seconds with a fractional part (for example 1706725399.280178).
  • symbol is the market for which pricing is being streamed.
  • exchange is the source of the pricing.
  • bids and asks are 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.
🔗

INFO

The 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:

  • timestamp is the time of the event as recorded by our trading engine, in Unix milliseconds (an integer).
  • last_trade_timestamp is the Unix time in milliseconds of the most recent trade on this order.
  • id is the order ID.
  • symbol is the market for which the order was placed.
  • status is the current status of the order. The available statuses are 'open', 'closed', 'canceled', and 'rejected'.
  • type is the type of order (always limit).
  • time_in_force indicates how long an order will remain active before it is executed or expires. It is always FOK (fill-or-kill): the order is filled immediately in full, or not at all.
  • side is the direction of the order (buy or sell).
  • price is the limit price of the order. It is distinct from the price the order was filled at.
  • average is the average price the order was filled at.
  • amount is the size of the base asset the order was placed for.
  • filled is the amount of base asset that was executed.
  • remaining is the amount of base asset that has not been executed.
  • cost is the amount of quote asset the order was filled for (filled × average). It will be zero if the order was not filled.
  • error_message is the reason the order was canceled or rejected. It is empty otherwise.
  • trades is a list of the fills for this order. Each item has the same fields as the data of a trade message. The list is empty ([]) while the order has no fills.
  • fee is the fee charged for an order, as a string.
  • client_order_id is 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:

  • timestamp is the time of the fill as recorded by our trading engine, in Unix milliseconds (an integer).
  • id is the trade ID.
  • order is the order ID associated with the fill.
  • symbol is the market for which the order was placed.
  • type is the type of order.
  • side is the direction of the order.
  • price is the price of this fill.
  • amount is the base-asset quantity of this fill.
  • cost is the quote-asset amount of this fill (amount × price).
  • taker_or_maker indicates whether the order was filled as an aggressor.
  • fee is the fee charged for this fill, as a string.
  • client_order_id is the client_order_id of 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"
   }
}

Did this page help you?