GuidesAPI Reference
DocumentationLog In

Websocket Feed

The websocket feed provides real-time market data updates for orders and trades.

📘

Websocket URL

wss://ws-feed.prime.coinbase.com

Overview

Real-time market data updates provide the fastest insight into order flow and trades.
This means that you are responsible for reading the message stream and using the message relevant for your needs which can include building real-time order books or tracking real-time trades.

Connections to the websocket feed are rate-limited to 1 per 4 seconds per IP; messages sent by the client on each connection are rate-limited to 100 per second per IP.

Protocol overview

The websocket feed uses a bidirectional protocol, which encodes all messages as JSON objects.
All messages have a type attribute that can be used to handle the message appropriately.

Please note that new message types can be added at any point in time.
Clients are expected to ignore messages they do not support.

Error messages:
Most failure cases will cause an error message (a message with the type "error") to be emitted.
This can be helpful for implementing a client or debugging issues.

{
  "type": "error",
  "message": "error message"
  /* ... */
}

Subscribe

// Request
// Subscribe to heartbeat channel, and
// receive any order updates for portfolio 12345
{
    type: "subscribe",
    channel: "heartbeats",
    access_key: ACCESS_KEY,
    api_key_id: SENDERCOMPID,
    passphrase: PASSPHRASE,
    portfolio_id: PORTFOLIO_ID,
    product_ids: ["BTC-USD"],
  }

To begin receiving feed messages, you must first send a subscribe message to the server indicating which channels and products to receive. This message is mandatory — you will be disconnected if no subscribe has been received within 5 seconds.

Every subscribe request must also be signed; see the section below on signing for more information.

// Response
{
  "type": "subscriptions",
  "channels": [
    {
      "name": "l2_data",
      "product_id": ["ETH-USD"]
    },
    {
      "name": "heartbeat"
    },
    {
      "name": "orders",
      "portfolio_id": ["12345"]
    }
  ]
}

Once a subscribe message is received the server will respond with a subscriptions message that lists all channels you are subscribed to.

Subsequent subscribe messages will add to the list of subscriptions.

If you want to unsubscribe from channel/product pairs, send an unsubscribe message.
The structure is equivalent to subscribe messages.

// Request
{
  "type": "unsubscribe",
  "product_ids": ["ETH-USD"],
  "channel": "l2_data"
}
// Request
{
  "type": "unsubscribe",
  "channels": ["heartbeat"]
}

As a response to an unsubscribe message you will receive a subscriptions message.

Signing Messages

timestamp = Time.now.to_i
string = "#{channelName}#{accessKey}#{svcAcctId}#{timestamp}#{portfolioId}";
# create a sha256 hmac with the secret key
hash = OpenSSL::HMAC.digest('sha256', @secret, string)
# pass this body with your requests
message = Base64.strict_encode64(hash)
async def sign(channel, key, secret, account_id, portfolio_id, product_ids):
    message = channel + key + account_id + time.strftime("%Y-%m-%dT%H:%M:%SZ",s) + portfolio_id + product_ids
    print(message)
    signature = hmac.new(secret.encode('utf-8'), message.encode('utf-8'), digestmod=hashlib.sha256).digest()
    signature_b64 = base64.b64encode(signature).decode()
    print(signature_b64)
    return signature_b64

You must authenticate yourself when subscribing to the WebSocket feed. To do so, you must construct a single payload, and then sign in.

Here's a list of the values which you will need for your payload:

  • channelName: the WebSocket feed to subscribe to
  • accessKey: your API key
  • svcAcctId: the service account ID or sender_comp_id
  • timestamp: an ISO (RFC3339) timestamp for your request
  • portfolioId / orderId: the portfolio or order ID (depending on which feed you're subscribing to)
  • products: a continuous string with no separators of products to subscribe to. For example "BTC-USDETH-USD"

The final prehash payload is generated by creating a concatenated string of channelName + accessKey + svcAccountId + timestamp + portfolioId + products (where + represents string concatenation). Apply a sha256 HMAC using your secret key on this prehash payload, and then base64-encode it as your final payload within your request.

JavaScript WebSocket Subscription Example

const WebSocket = require('ws');
const CryptoJS = require('crypto-js');

// Derived from your Coinbase Prime API Key
//  SIGNING_KEY: the signing key provided as a part of your API key. Also called the "SECRET KEY"
//  ACCESS_KEY: the access key provided as a part of your API key
//  PASSPHRASE: the PASSPHRASE key provided as a part of your API key
//  SENDERCOMPID: also called the "Service Account Id"
const SIGNING_KEY = process.env.API_SECRET_KEY;
const ACCESS_KEY = process.env.API_ACCESS_KEY;
const PASSPHRASE = process.env.PASSPHRASE;
const SENDERCOMPID = process.env.SENDERCOMPID;

if (!SIGNING_KEY || !ACCESS_KEY || !PASSPHRASE || !SENDERCOMPID) {
  throw new Error('missing mandatory environment variable(s)');
}

// A specific portfolio ID (only necessary if relevant to the request you're making)
const PORTFOLIO_ID = process.env.PORTFOLIO_ID;

// The base URL of the API
const PROD_URL = 'wss://ws-feed.prime.coinbase.com';

// Function to generate a signature using CryptoJS
function sign(str, secret) {
  const hash = CryptoJS.HmacSHA256(str, secret);
  return hash.toString(CryptoJS.enc.Base64);
}

function timestampAndSign(message, portfolioId, channel, products = []) {
  const now = new Date(new Date().toUTCString());
  const timeUnix = Math.floor(now.getTime() / 1000);
  // js includes milliseconds in the ISO string which make it invalid.. trim that part out.
  const timeISO = `${now.toISOString().split('.')[0]}Z`;

  const strToSign = `${channel}${ACCESS_KEY}${SENDERCOMPID}${timeISO}${portfolioId}${products.join(
    '',
  )}`;

  const sig = sign(strToSign, SIGNING_KEY);

  return { ...message, signature: sig, timestamp: timeUnix };
}

const ws = new WebSocket(PROD_URL);

ws.on('open', function open() {
  const portfolioId = PORTFOLIO_ID;
  const products = ['BTC-USD'];
  const channelName = 'heartbeats';

  const message = {
    type: 'subscribe',
    channel: channelName,
    access_key: ACCESS_KEY,
    api_key_id: SENDERCOMPID,
    passphrase: PASSPHRASE,
    portfolio_id: portfolioId,
    product_ids: products,
  };
  const subscribeMsg = timestampAndSign(message, portfolioId, 'heartbeats', products);
  ws.send(JSON.stringify(subscribeMsg));
});

ws.on('message', function message(data) {
  console.log('received: %s', data);
});

Sequence Numbers

Most feed messages contain a sequence number.
Sequence numbers are increasing integer values for each product with every new message being exactly 1 sequence number greater than the one before it.

If you see a sequence number that is more than one value from the previous, it means a message has been dropped.
A sequence number less than one you have seen can be ignored or has arrived out-of-order.
In both situations you may need to perform logic to make sure your system is in the correct state.

While a websocket connection is over TCP, the websocket servers receive market data in a manner which can result in dropped messages. Your feed consumer should either be designed to expect and handle sequence gaps and out-of-order messages, or use channels that guarantee delivery of messages.

If you want to keep an order book in sync, consider using the level 2 channel, which provides such guarantees.

Channels

The heartbeat channel

// Request
{
  "type": "subscribe",
  "channels": [{ "name": "heartbeats" }]
}

Heartbeats indicate the current timestamp, as well as the number of messages sent.

// Heartbeat message
{
  "current_time": "2014-11-07T08:19:28.464459Z",
  "heartbeat_counter": 4
}

The orders channel

The orders channel provides real-time updates on orders you've made.

{
  "order_id": "12345",
  "client_order_id": "9999",
  "cum_qty": "23",
  "leaves_qty": "10",
  "avg_px": "124.55",
  "last_qty": "100",
  "last_px": "124.57",
  "fees": "0.28",
  "status": "filled"
}

The l2_data channel

The easiest way to keep a snapshot of the order book is to use the l2_data channel. It guarantees delivery of all updates.

{
  "type": "snapshot",
  "product_id": "BTC-USD",
  "base_min_size": "0.01",
  "base_max_size": "10000",
  "base_increment": "0.0001",
  "quote_min_size": "10",
  "quote_max_size": "10000000",
  "quote_increment": "0.01"
}

When subscribing to the channel, it will send a message with the type snapshot and the corresponding product_id.
bids and asks are arrays of [price, size] tuples and represent the entire order book.

{
  "type": "l2_data",
  "product_id": "BTC-USD",
  "event_time": "2019-08-14T20:42:27.265Z",
  "updates": [
    {
      "side": "buy",
      "px": "10101.80000000",
      "qty": "0.162567"
    }
  ]
}

Calculating Slippage with the WebSocket Feed

#!/usr/bin/env python
import asyncio
import datetime
import json, hmac, hashlib, time, base64
import asyncio
import time
import websockets
import unittest
import logging
import sys

PASSPHRASE = "<API key passphrase here>"
ACCESS_KEY = "<API access key here>"
SIGNING_KEY = "<API signing key here>"
SVC_ACCOUNTID = "<API account ID passphrase here>"
s = time.gmtime(time.time())
TIMESTAMP = time.strftime("%Y-%m-%dT%H:%M:%SZ",s)

"""
A processor maintains an in-memory price book used for analytics
"""
class PriceBookProcessor:
    def __init__(self, snapshot):
        """
        Instantiate a processor using a snapshot from the Websocket feed
        """
        self.bids = []
        self.offers = []
        snapshot_data = json.loads(snapshot)
        px_levels = snapshot_data["events"][0]["updates"]
        for i in range(len(px_levels)):
            level = px_levels[i]
            if level["side"] == "bid":
                self.bids.append(level)
            elif level["side"] == "offer":
                self.offers.append(level)
            else:
                raise IOError()
        self._sort()
    def apply_update(self, data):
        """
        Update in-memory state with a single update from the Websocket feed
        """
        event = json.loads(data)
        if event["channel"] != "l2_data":
            return
        events = event["events"]
        for e in events:
            updates = e["updates"]
            for update in updates:
                self._apply(update)
        self._filter_closed()
        self._sort()
    def _apply(self, level):
        if level["side"] == "bid":
            found = False
            for i in range(len(self.bids)):
                if self.bids[i]["px"] == level["px"]:
                    self.bids[i] = level
                    found = True
                    break
            if not found:
                self.bids.append(level)
        else:
            found = False
            for i in range(len(self.offers)):
                if self.offers[i]["px"] == level["px"]:
                    self.offers[i] = level
                    found = True
                    break
            if not found:
                self.offers.append(level)
    def _filter_closed(self):
        self.bids = [x for x in self.bids if abs(float(x["qty"])) > 0]
        self.offers = [x for x in self.offers if abs(float(x["qty"])) > 0]
    def _sort(self):
        self.bids = sorted(self.bids, key=lambda x: float(x["px"]) * -1)
        self.offers = sorted(self.offers, key=lambda x: float(x["px"]))
    def stringify(self):
        """
        Return a string summary of the contents of the price book
        """
        best_bid = self.bids[0]["px"]
        best_offer = self.offers[0]["px"]
        spread = str(float(best_offer) - float(best_bid))
        l1 = f"{best_bid} =>{spread}<= {best_offer} ({len(self.bids)} bids, {len(self.offers)} offers)\n"
        bids = self.bids[:5]
        offers = self.offers[:5]
        l2, l3 = "", ""
        if len(bids) > 0:
            l2 = "Bids: " + ", ".join([b["qty"] + " " + b["px"] for b in bids]) + "\n"
        if len(offers) > 0:
            l3 = "Offers: " + ", ".join([b["qty"] + " " + b["px"] for b in offers]) + "\n"
        l4 = "Buy 1 BTC: " + str(self.estimate_aggressive_px(1.0, True)) + " USD\n"
        return l1 + l2 + l3 + l4
    def estimate_aggressive_px(self, qty, is_bid=True):
        """
        Estimate the average price of an aggressive order of a given size/side
        """
        orders = self.bids
        if is_bid:
            orders = self.offers
        total, total_value = 0.0, 0.0
        idx = 0
        while total < qty and idx < len(orders):
            px = float(orders[idx]["px"])
            this_level = min(qty-total, float(orders[idx]["qty"]))
            value = this_level * px
            total_value += value
            total += this_level
            idx += 1
        return total_value / total

"""
Sign a subscription for the Websocket API
"""
async def sign(channel, key, secret, account_id, portfolio_id, product_ids):
    message = channel + key + account_id + TIMESTAMP + portfolio_id + product_ids
    print(message)
    signature = hmac.new(secret.encode('utf-8'), message.encode('utf-8'), digestmod=hashlib.sha256).digest()
    signature_b64 = base64.b64encode(signature).decode()
    print(signature_b64)
    return signature_b64


"""
Main loop for consuming from the Websocket feed
"""
async def main_loop():
    uri = "wss://ws-feed.prime.coinbase.com"
    async with websockets.connect(uri, ping_interval=None, max_size=None) as websocket:
        signature = await sign('l2_data', ACCESS_KEY, SIGNING_KEY, SVC_ACCOUNTID, "", "BTC-USD")
        print(signature)
        auth_message = json.dumps({
            "type": "subscribe",
            "channel": "l2_data",
            "access_key": ACCESS_KEY,
            "api_key_id": SVC_ACCOUNTID,
            "timestamp": TIMESTAMP,
            "passphrase": PASSPHRASE,
            "signature": signature,
            "portfolio_id": "",
            "product_ids": ["BTC-USD"],
        })

        print(type(auth_message))
        print(auth_message)

        await websocket.send(auth_message)
        try:
            processor = None
            while True:
                response = await websocket.recv()
                #print(f"<<< {response}")
                parsed = json.loads(response)
                if parsed["channel"] == "l2_data" and parsed["events"][0]["type"] == "snapshot":
                    processor = PriceBookProcessor(response)
                elif processor != None:
                    processor.apply_update(response)

                if processor != None:
                    print(processor.stringify())
                    sys.stdout.flush()

        except websockets.exceptions.ConnectionClosedError:
            print("Error caught")
            sys.exit(1)


if __name__ == '__main__':
    asyncio.run(main_loop())