Websockets

🚧

Mainnet Websockets are only available on Pro Plan

Sepolia (testnet) websockets are available on our Free Plan. Mainnet chains (eg. Ethereum) require the Pro Plan or higher.

Learn more about Reservoir subscription plans here.

Overview

The Reservoir websocket service allows developers to establish a connection and obtain real-time event updates, eliminating the requirement for constant polling. You can subscribe to events, and optionally provide a filter object to narrow down events based on specific tags.

There are two types of events, core events, and derived events. Core events are pure events on the first layer of data, such as a new sale, listing, transfer, and are useful for syncing data streams. Derived events build ontop of our first layer data by optimizing for certain use cases. For example, our top-bid.changed event is useful for notifying users when the top bid of a collection changes.

Please note, the current limit is 20 connections and 1,000 subscription per connection. However, subscribing to a large number of tags may affect the performance of your Node.js server and the websocket connection. It's recommended to keep the number of subscriptions within a reasonable limit to ensure smooth operation.

Refer to the table below for a list of supported events and tags.

Core Events

EventTagsEvent Description
ask.createdcontract,source,maker,takerTriggered on new ask creation.
ask.updatedcontract,source,maker,takerTriggered on ask update (filled, expiry, cancellation, approval, balance change).
bid.createdcontract,source,maker,takerTriggered on new bid creation.
bid.updatedcontract,source,maker,takerTriggered on bid update (filled, expiry, cancellation, approval, balance change).
sale.createdcontract,maker,takerTriggered on a new sale.
sale.updatedcontract,maker,takerTriggered when we update a sale.
sale.deletedcontract,maker,takerTriggered when a sale gets deleted (in case of chain re-organization).
transfer.createdaddress,from,toTriggered when a transfer is created.
transfer.updatedaddress,from,toTriggered when we update a transfer.
transfer.deletedaddress,from,toTriggered when an nft transfer is deleted (re-org).
token.createdcontractTriggered when a token is created (minted).
token.updatedcontractTriggered when a token is updated.
token-attribute.createdcontractTriggered when a token attribute is created.
token-attribute.deletedcontractTriggered when a token attribute is updated.
collection.createdidTriggered when a collection is created (first token minted)
collection.updatedidTriggered when a collection is updated.

We also allow subscribing with *to subscribe to all core events. Additionally, event.* subscribes to all of the various events under a main event, for example sale.* will subscribe you to sale.created, sale.updated,sale.deleted. Filtering is also supported, but please note that there might be unexpected behavior from filtering on all core events via * due to the different events having variable tags for filtering.

Derived Events

EventTagsEvent Description
top-bid.changedcontract,sourceTriggered on new top bid change.

Authentication

The Websocket Service utilizes the same API key for authentication as our other APIs, and it does not impact the rate limits associated with your API key tier.

ChainUrl
Mainnetwss://ws.reservoir.tools
Sepolia [testnet]wss://ws-sepolia.reservoir.tools
Polygonwss://ws-polygon.reservoir.tools
Mumbaiwss://ws-mumbai.reservoir.tools
Basewss://ws-base.reservoir.tools
Base Sepolia [testnet]wss://ws-base-sepolia.reservoir.tools
Arbitrumwss://ws-arbitrum.reservoir.tools
Arbitrum Novawss://ws-zora.reservoir.tools
Optimismwss://ws-optimism.reservoir.tools
BNB Smart Chainwss://ws-bsc.reservoir.tools
Zora Networkwss://ws-zora.reservoir.tools
Zora Goerli [testnet]wss://ws-zora-testnet.reservoir.tools
Polygon zkEVMwss://ws-polygon-zkevm.reservoir.tools
zkSyncwss://ws-zksync.reservoir.tools
Blastwss://ws-blast.reservoir.tools

To connect, provide your API key as a query param in the connection url:

const wss = new ws(`wss://ws.reservoir.tools?api_key=${YOUR_API_KEY}`);

🚧

The websocket server needs to restart to deploy updates

The websocket server uses a rolling restart mechanism to deploy new changes. This will disconnect you and require you to reconnect to the websocket server. There is no downtime, as you are only disconnected once the new server is up and running. Please make sure to include logic in your implementation to handle these restarts, and reconnect accordingly.

Interacting with the Websocket

Before sending any messages over the Websocket, wait for a ready message back from the server. The message looks like:

{
  	// The type of operation
	"type": "connection",
   	// The status of the operation
	"status": "ready",
	"data": {
     	// Your socket id for debugging purposes
		"id": "9nqpzwmwh86"
	}
}

Subscribing

Below are examples on how to subscribe to a websocket. Besides the websocket alone, you can choose filters, exclusions, and changed types.

Subscription (General)

{
	"type": "subscribe",
	"event": "ask.created",
}

Subscription with Filters

{
  "type": "subscribe",
  "event": "collection.updated",
  "filters": {
    "id": "0xd774557b647330c91bf44cfeab205095f7e6c367"
    // can optionally filter by multiple tags at once by using an array
    // "contract": [
    //		"0x0000c3Caa36E2d9A8CD5269C976eDe05018f0000",
    //		"0x0000c4Caa36E2d9A8CD5269C976eDe05018f0000"
    // ]
  }
}

Subscription with Exclude

You can also exclude events by certain sources when the source is a valid tag.

{
  "type": "subscribe",
  "event": "bid.created",
  "exclude": {
    "source": ["opensea.io"]
  }
}

Subscription with Changed

This is to filter by what changed in .updated events

{
  "type": "subscribe",
  "event": "token.updated",
  "changed": "collection_id"
}

Subscription Response

In you're response, your filters, changed, and exclude will be repeated back. This is an example of what might be returned.

{
	"type": "subscribe",
	"status": "success",
	"data": {
		"event": "ask.created",
    	// Blank object if no filter is passed
		"filters": {
			"contract": "0x404e739c9d383efc36d3ee85fdceed53212104b2"
		}
	}
}

Note: While there are no hard limits for number of subscriptions you can have, we recommend to manage your subscriptions efficiently and unsubscribe from channels when they are no longer needed.

Unsubscribing

{
	"type": "unsubscribe",
	"event": "ask.created",
  	// Tags to filter events by, leave blank to not filter by any tags
 	"filters": {
   		"contract": "0x0000c3Caa36E2d9A8CD5269C976eDe05018f0000"
 	 }
}

Unsubscribe Response

{
	"type": "unsubscribe",
	"status": "success",
	"data": {
		"event": "top-bid.changed"
	}
}

Connecting Examples

Javascript

npm install ws
const ws = require('ws');

// You can get your API key from https://reservoir.tools
const YOUR_API_KEY = '';

// sepolia: wss://ws.ws-sepolia.reservoir.tools
const wss = new ws(`wss://ws.reservoir.tools?api_key=${YOUR_API_KEY}`);

wss.on('open', function open() {
    console.log('Connected to Reservoir');

    wss.on('message', function incoming(data) {
        console.log('Message received: ', JSON.stringify(JSON.parse(data)));

        // When the connection is ready, subscribe to the top-bids event
        if (JSON.parse(data).status === 'ready') {
            console.log('Subscribing');
            wss.send(
                JSON.stringify({
                    type: 'subscribe',
                    event: 'top-bid.changed',
                }),
            );

            // To unsubscribe, send the following message
            // wss.send(
            //     JSON.stringify({
            //         type: 'unsubscribe',
            //         event: 'top-bid.changed',
            //     }),
            // );
        }
    });
});

Python

pip install websockets
import asyncio
import json
import websockets

# You can get your API key from https://reservoir.tools
api_key = ''

# sepolia: wss://ws.ws-sepolia.reservoir.tools
wss_url = f'wss://ws.reservoir.tools?api_key={api_key}'

async def subscribe_top_bids():
    async with websockets.connect(wss_url) as websocket:
        print('Connected to Reservoir')

        async for data in websocket:
            message = json.loads(data)
            print('Message received:', message)

            if message.get('status') == 'ready':
                print('Subscribing')
                await websocket.send(
                    json.dumps({
                        'type': 'subscribe',
                        'event': 'top-bid.changed',
                    }),
                )

                # To unsubscribe, send the following message
                # await websocket.send(
                #     json.dumps({
                #         'type': 'unsubscribe',
                #         'event': 'top-bid.changed',
                #     }),
                # )

asyncio.run(subscribe_top_bids())

Golang

go get nhooyr.io/websocket
package main

import (
  "context"
  "fmt"
  "log"
  "net/http"
  "time"

  "nhooyr.io/websocket"
  "nhooyr.io/websocket/wsjson"
)

// You can get your API key from https://reservoir.tools
const apiKey = ""

func main() {
  ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
  defer cancel()

  // sepolia: wss://ws.ws-sepolia.reservoir.tools
  url := fmt.Sprintf("wss://ws.reservoir.tools?api_key=%s", apiKey)
  conn, _, err := websocket.Dial(ctx, url, &websocket.DialOptions{
    HTTPHeader: http.Header{
      "Authorization": []string{fmt.Sprintf("Bearer %s", apiKey)},
    },
  })
  if err != nil {
    log.Fatal("Error connecting to Reservoir: ", err)
  }
  defer conn.Close(websocket.StatusInternalError, "the sky is falling")

  fmt.Println("Connected to Reservoir")

  var message map[string]interface{}
  for {
    err := wsjson.Read(ctx, conn, &message)
    if err != nil {
      log.Println("Error reading message: ", err)
      break
    }

    fmt.Println("Message received: ", message)

    // When the connection is ready, subscribe to the top-bids event
    if message["status"] == "ready" {
      fmt.Println("Subscribing")
      err = wsjson.Write(ctx, conn, map[string]interface{}{
        "type":  "subscribe",
        "event": "top-bid.changed",
      })
      if err != nil {
        log.Println("Error subscribing: ", err)
        break
      }

      // To unsubscribe, send the following message
      // err = wsjson.Write(ctx, conn, map[string]interface{}{
      //     "type":    "unsubscribe",
      //     "event": "top-bid.changed",
      // })
      // if err != nil {
      //     log.Println("Error unsubscribing: ", err)
      //     break
      // }
    }
  }
}

Rust

[dependencies]
serde_json = "1.0.73"
use serde_json::json;
use ws::{Builder, CloseCode, Error, Handler, Message, Result, Sender};

struct Client {
    out: Sender,
}

impl Handler for Client {
    fn on_open(&mut self, _: Handshake) -> Result<()> {
        println!("Connected to Reservoir");
        Ok(())
    }

    fn on_message(&mut self, msg: Message) -> Result<()> {
        let message: serde_json::Value = serde_json::from_str(&msg.to_string()).unwrap();
        println!("Message received: {:?}", message);

        if message["status"] == "ready" {
            println!("Subscribing");
            self.out.send(json!({
                "type": "subscribe",
                "event": "top-bid.changed",
            }))?;

            // To unsubscribe, send the following message
            // self.out.send(json!({
            //     "type": "unsubscribe",
            //     "event": "top-bid.changed",
            // }))?;
        }
        Ok(())
    }

    fn on_close(&mut self, code: CloseCode, reason: &str) {
        match code {
            CloseCode::Normal => println!("Connection closed normally"),
            CloseCode::Away => println!("Connection closed because server is going away"),
            _ => println!("Connection closed with code: {:?} for reason: {}", code, reason),
        }
    }

    fn on_error(&mut self, err: Error) {
        println!("Error: {:?}", err);
    }
}

fn main() {
    // You can get your API key from https://reservoir.tools
    let api_key = "";

    // sepolia: wss://ws.ws-sepolia.reservoir.tools
    let url = format!("wss://ws.reservoir.tools?api_key={}", api_key);

    Builder::new()
        .build(|out| Client { out })
        .unwrap()
        .connect(url)
        .unwrap();
}