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.

Sending Messages

Authentication

To authenticate, the initial connection must contain the same headers as a REST request. After the connection is authenticated there is no further need to send the headers.

import hmac
import json
import time
import hashlib
import requests
import threading
from websockets.sync.client import connect

class ZerocapWebsocketClient:
    def __init__(self, api_key: str, api_secret: str, env: str='prod'):
        self.api_key = api_key
        self.api_secret = api_secret
        self.websocket = None

        if env == 'prod':
            self.base_url = "wss://dma-ws.zerocap.com/v2"
            self.http_url = "https://dma-api.zerocap.com/v2/orders"
        elif env == 'sandbox':
            self.base_url = "wss://sandbox-ws.zerocap.com/v2"
            self.http_url = "https://sandbox-api.zerocap.com/v2/orders"
        
    def verify_identity(self):
        timestamp = int(time.time())
        headers = {'Content-Type': 'application/json'}
        data = {"api_key": self.api_key, "signature": self.hashing(timestamp)}
        url = f"{self.http_url}/api_key_signature_valid"
        response = requests.post(url, data=json.dumps(data), headers=headers)
        if response.status_code != 200 or response.json().get('status_code') != 200:
            raise Exception("Authentication failed")
        
    def hashing(self, timestamp):
        return hmac.new(
            self.api_secret.encode("utf-8"), str(timestamp).encode("utf-8"), hashlib.sha256
        ).hexdigest()

    def close(self):
        try:
            if self.websocket:
                self.websocket.close()
        except:
            pass

    def recv(self, websocket):
        return json.loads(websocket.__next__())

    def send(self, message: json):
        try:
            self.websocket.send(json.dumps(message))
        except Exception as e:
            raise Exception(e)

    def test_send_heartbeat(self):
        while True:
            time.sleep(5)
            if not self.websocket:
                continue
            try:
                self.websocket.send(json.dumps({"type": "message", "message": "ping"}))
            except Exception as e:
                raise Exception(e)
        return

    def create_connection(self, timestamp: int = 0):
        if not timestamp:
            timestamp = int(time.time())

        try:
            threading.Thread(target=self.test_send_heartbeat).start()

            with connect(self.base_url,
                         additional_headers={"api-key": self.api_key, "signature": self.hashing(timestamp), "timestamp": str(timestamp)}
                         ) as self.websocket:
                while True:
                    yield self.websocket.recv()

        except Exception as e:
            self.close()
            raise Exception(e)

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.

Channels

Heartbeat

You must send a heartbeat every 30 seconds or you will be disconnected.

// Request
{
    "type": "message",
    "message": "ping"
}

A heartbeat message is of the type message and has one parameter:

  • message - will always have value pong.
// Response
{
    "type": "message",
    "message": "pong"
}

Market Data

The price channel streams executable pricing for a given market.

// Request
{
    "type": "price",
    "symbols": ["USDT/AUD"]
}

Multiple markets can also be subscribed at the same time.

// 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.
  • symbol is the market for which pricing is being streamed.
  • 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 orders channel streams order updates.

// 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.
  • last_trade_timestamp is the Unix timestamp 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.
  • time_in_force indicates how long an order will remain active before it is executed or expires.
  • side is the direction of the order.
  • price is the price of the order if it was a limit 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 size of the quote asset the order the order was filled at. It will be zero if the order was not filled.
  • trades are the fills that correspond to a given order.
  • fee is the fee charged for an order.
  • 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":"open",
      "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":149.77,
      "error_message":"",
      "fee":"0",
      "trades":[],
      "client_order_id":"79de03ab-c2ec-4dd5-8bce-29cbe1b70fca"
   }
}

Trade

The trades channel streams fills

// Request
{
    "type": "trade"
}

The trades channel sends a message with the following fields:

  • timestamp is the time of the event as recorded by our trading engine.
  • 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 average price the order was filled at.
  • amount is the size of the base asset the order was placed for.
  • cost is the size of the quote asset the order the trade was filled at.
  • taker_or_maker indicates whether the order was filled as an aggressor.
  • fee is the fee charged for an order.
  • client_order_id is an optional field for the user to give placed orders a unique identifier.
// 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"
   }
}