Skip to main content
Alpha Feature — The Pure WebSocket API is currently in active development and is not production-stable. It may be unstable, change without notice, or be removed entirely at any time. Do not build critical production workflows on it. Contact [email protected] to be notified of changes.

Overview

The Pure WebSocket API delivers real-time orderbook events as they happen. Instead of polling the REST API, you open a single persistent connection and subscribe to the specific streams you care about. Every time a listing or offer is created, updated, or removed, your client receives a push notification within milliseconds. What you can subscribe to:
Event typeDescription
listingsSell-side orderbook — new listings, price changes, quantity changes, deletions
offersBuy-side orderbook — new offers, price changes, quantity changes, deletions

Connection

Connect to the WebSocket server by opening a standard WebSocket connection with your API key passed as a query parameter:
wss://wscollectpure.com?apiKey=YOUR_API_KEY
Connection parameters:
ParameterTypeRequiredDescription
apiKeystringYesYour Pure API key. Passed as a query parameter during the HTTP upgrade.
compressedbooleanNoSet to true to receive messages as zstd-compressed binary frames instead of plain text JSON. See Compression below.
Connection with compression enabled:
wss://wscollectpure.com?apiKey=YOUR_API_KEY&compressed=true

Authentication

Authentication happens at the HTTP upgrade step before the WebSocket handshake completes. If the API key is missing or invalid, the server rejects the upgrade with 401 Unauthorized and the connection is never established. There is no post-connection authentication step.

Idle Timeout

The server closes idle connections after 10 seconds of inactivity. A connection is considered inactive if it receives no data (messages, pings, or pongs) within that window. To keep your connection alive, either:
  • Subscribe to an active high-frequency topic (listings or offers on a liquid product) so you receive regular events
  • Send a WebSocket ping frame from your client every 8–9 seconds
Most WebSocket client libraries respond to server pings automatically (sending a pong), but they do not always send their own proactive pings. Check your library’s documentation.

Sending Messages

All messages sent to the server must be JSON text frames. Binary frames are ignored.

Message Format

{
  "action": "subscribe" | "unsubscribe",
  "event": "listings" | "offers",
  "filter": {
    "productId": "uuid",
    "variantId": "uuid"
  }
}
FieldTypeRequiredDescription
action"subscribe" | "unsubscribe"YesWhether to start or stop receiving events for this stream
event"listings" | "offers"YesThe stream to subscribe or unsubscribe from
filterobject | "" | omittedNoNarrow the subscription to a specific product or variant. Omit or pass "" for the global stream. See Subscription Filters below.

Subscriptions

How Subscriptions Work

Each subscription maps to an internal topic. When a server-side event occurs (e.g. a listing is updated), the server publishes to all matching topics simultaneously. Your connection receives the event envelope on every topic it is subscribed to. You can hold multiple active subscriptions per connection simultaneously — for example, you can subscribe to the global offers stream and a product-scoped listings stream at the same time. Duplicate subscriptions are rejected. Sending a subscribe message for a topic you are already subscribed to returns an error response.

Topic Namespace

The server resolves your subscription request to one of the following internal topics based on the event and filter:
SubscriptionResolved topicReceives
event: "listings", no filterglobal:listingsAll listing events across the entire marketplace
event: "listings", filter: { productId }listings:product:{id}Listing events for that product only
event: "listings", filter: { productId, variantId }listings:product:{id}:variant:{id}Listing events for that product+variant combination only
event: "offers", no filterglobal:offersAll offer events across the entire marketplace
event: "offers", filter: { productId }offers:product:{id}Offer events for that product only
event: "offers", filter: { productId, variantId }offers:product:{id}:variant:{id}Offer events for that product+variant combination only

Subscription Filters

Filters let you narrow a subscription to a specific product or variant. Without a filter you receive events for the entire marketplace.
FilterExampleEffect
Omitted / ""{}Global — all events for that stream
{ productId }{ "productId": "abc-123" }Only events for the given product
{ productId, variantId }{ "productId": "abc-123", "variantId": "var-456" }Only events for that specific variant of that product
You cannot filter by variantId alone. A variantId filter is only valid when productId is also present. Sending { "variantId": "..." } without a productId silently falls back to the global topic.

Subscription Examples

Subscribe to all marketplace listings
{
  "action": "subscribe",
  "event": "listings"
}
Subscribe to offers for one product
{
  "action": "subscribe",
  "event": "offers",
  "filter": { "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }
}
Subscribe to listings for a specific variant
{
  "action": "subscribe",
  "event": "listings",
  "filter": {
    "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "variantId": "1e6c3d8a-8e24-4f1a-a1c2-ef2b9b3c0d91"
  }
}
Unsubscribe from a stream
{
  "action": "unsubscribe",
  "event": "listings"
}

Server Responses

The server sends four categories of messages back to your client.

Subscription Confirmed

Sent immediately after a successful subscribe action.
{
  "type": "subscribed",
  "event": "offers",
  "filter": { "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" },
  "topic": "offers:product:3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
FieldTypeDescription
type"subscribed"Message type identifier
eventstringThe event stream you subscribed to
filterobject | nullThe filter applied, or null for a global subscription
topicstringThe resolved internal topic name

Unsubscription Confirmed

Sent immediately after a successful unsubscribe action.
{
  "type": "unsubscribed",
  "event": "offers",
  "filter": null,
  "topic": "global:offers"
}

Error

Sent when a message cannot be processed — malformed JSON, an unknown action or event type, a duplicate subscription, or an invalid filter.
{
  "type": "error",
  "message": "Invalid action: \"ping\". Must be one of: subscribe, unsubscribe"
}
Common error messages:
MessageCause
"Missing required field: action"The action field is absent or not a string
"Invalid action: ..."The action value is not subscribe or unsubscribe
"Missing required field: event"The event field is absent or not a string
"Invalid event: ..."The event value is not listings or offers
"filter must be an object, empty string, or omitted"The filter field has an invalid type
"filter.variantId requires filter.productId"variantId was provided without productId
"Already subscribed to ..."Duplicate subscription attempted
"Malformed JSON"The message body is not valid JSON

Realtime Event

The main payload. Sent whenever an orderbook event occurs that matches your subscriptions.
{
  "event": "listing.created",
  "action": "created",
  "data": { ... },
  "changedFields": null,
  "subscription": {
    "topic": "global:listings",
    "filter": null
  },
  "receivedAt": "2026-02-26T14:23:01.452Z",
  "requestId": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}
FieldTypeDescription
eventstringDot-notation event name, e.g. listing.created, offer.updated
action"created" | "updated" | "deleted"The change that occurred
dataobjectThe record payload. Shape depends on the event type — see Data Schemas below
changedFieldsstring[] | nullOn updated events, an array of the field names that changed. Always null for created and deleted events
subscription.topicstringThe internal topic that triggered this delivery
subscription.filterobject | nullThe filter associated with this topic, or null for global topics
receivedAtstringISO 8601 timestamp of when the server received the upstream event
requestIdstring (UUID v4)A unique ID for this source event. The same requestId appears on every topic that the event is published to — use it for deduplication if you hold overlapping subscriptions
Event names:
Event nameTrigger
listing.createdA new sell listing was posted
listing.updatedAn existing listing changed (price, quantity, expiry, etc.)
listing.deletedA listing was cancelled or expired
offer.createdA new buy offer was placed
offer.updatedAn existing offer changed
offer.deletedAn offer was cancelled or expired

Data Schemas

Listing (data on listing events)

Sensitive seller information is stripped before delivery. The following fields are never present in the WebSocket payload: seller_id, seller_notes, internal_id, inventory_id.
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "product_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "variant_id": "1e6c3d8a-8e24-4f1a-a1c2-ef2b9b3c0d91",
  "price": 250000,
  "quantity": 2,
  "type": "fixed_pricing",
  "active": true,
  "premium_percent": 5.0,
  "premium_dollar": 100,
  "spot_premium": 4.5,
  "spot_premium_dollar": 80,
  "expires_at": "2026-03-01T00:00:00.000Z",
  "created_at": "2026-02-25T00:00:00.000Z",
  "updated_at": "2026-02-26T00:00:00.000Z"
}
FieldTypeDescription
idstring (UUID)Unique identifier for this listing
product_idstring (UUID)The product this listing is for
variant_idstring (UUID)The specific variant (e.g. coin grade/size)
priceinteger | nullAsk price in cents (e.g. 250000 = $2,500.00). May be null for spot-premium priced listings
quantityintegerNumber of units available
typestringPricing type: "fixed_pricing" or "premium_percentage_pricing"
activebooleanWhether the listing is currently active. Only active listings with quantity > 0 are emitted on created/updated events
premium_percentnumberPercentage premium over spot (e.g. 5.0 = 5%)
premium_dollarnumber | nullDollar premium over spot
spot_premiumnumber | nullSpot premium in percentage terms
spot_premium_dollarnumberSpot premium expressed in dollars
expires_atstring (ISO 8601)When this listing expires
created_atstring (ISO 8601)When the listing was created
updated_atstring (ISO 8601)When the listing was last modified
Prices are in cents (integer). Divide by 100 to get the dollar amount. This differs from the REST API, which returns prices already converted to dollars.

Offer (data on offer events)

Sensitive buyer information is stripped before delivery. The following fields are never present: buyer_id, buyer_notes, internal_id.
{
  "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "product_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "variant_id": "1e6c3d8a-8e24-4f1a-a1c2-ef2b9b3c0d91",
  "price": 240000,
  "quantity": 1,
  "type": "premium_percentage_pricing",
  "active": true,
  "premium_percent": 3.0,
  "premium_dollar": 0,
  "spot_premium": 3.5,
  "spot_premium_dollar": 70,
  "expires_at": "2026-03-01T00:00:00.000Z",
  "created_at": "2026-02-25T00:00:00.000Z",
  "updated_at": "2026-02-26T00:00:00.000Z"
}
FieldTypeDescription
idstring (UUID)Unique identifier for this offer
product_idstring (UUID)The product this offer is for
variant_idstring (UUID)The specific variant
priceinteger | nullBid price in cents. May be null for spot-premium priced offers
quantityintegerNumber of units the buyer wants
typestringPricing type: "fixed_pricing" or "premium_percentage_pricing"
activebooleanWhether the offer is currently active
premium_percentnumberPercentage premium the buyer is willing to pay over spot
premium_dollarnumberDollar premium over spot
spot_premiumnumber | nullSpot premium in percentage terms
spot_premium_dollarnumberSpot premium expressed in dollars
expires_atstring (ISO 8601)When this offer expires
created_atstring (ISO 8601)When the offer was placed
updated_atstring (ISO 8601)When the offer was last modified

Deduplication with requestId

When a single orderbook event matches multiple topics (e.g. a listing update on prod-1/var-1 gets published to global:listings, listings:product:prod-1, and listings:product:prod-1:variant:var-1), your connection can receive the same logical event multiple times if you are subscribed to more than one matching topic. Each event emission shares the same requestId UUID. Use it to deduplicate:
const seen = new Set();

ws.on("message", data => {
  const msg = JSON.parse(data.toString());
  if (!msg.requestId) return; // control message

  if (seen.has(msg.requestId)) return; // already processed
  seen.add(msg.requestId);

  // ... handle event once
});

Compression

For high-frequency subscriptions, you can enable zstd compression to reduce bandwidth. Pass ?compressed=true in the connection URL. When enabled:
  • All messages from the server (control responses and event payloads) arrive as binary frames, zstd-compressed
  • You must decompress each frame before JSON parsing
  • Sending messages to the server is unchanged — always plain text JSON
The ws Node.js library and the Python websockets library both support binary frames natively. You need to add a zstd decompression step using a library like @mongodb-js/zstd (Node) or zstandard / pyzstd (Python).

Code Examples

Node.js

Uses the ws package.
npm install ws
const WebSocket = require("ws");

const API_KEY = "YOUR_API_KEY";
const ws = new WebSocket(`wss://wscollectpure.com?apiKey=${API_KEY}`);

// Keep the connection alive — server closes idle connections after 10 s
const keepAlive = setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.ping();
  }
}, 8000);

ws.on("open", () => {
  console.log("Connected to Pure WebSocket");

  // Subscribe to all marketplace listings (global)
  ws.send(
    JSON.stringify({
      action: "subscribe",
      event: "listings",
    })
  );

  // Subscribe to offers for a specific product
  ws.send(
    JSON.stringify({
      action: "subscribe",
      event: "offers",
      filter: { productId: "3fa85f64-5717-4562-b3fc-2c963f66afa6" },
    })
  );
});

ws.on("message", raw => {
  const msg = JSON.parse(raw.toString());

  // Control messages have a `type` field
  if (msg.type === "subscribed") {
    console.log(`✓ Subscribed to ${msg.topic}`);
    return;
  }
  if (msg.type === "unsubscribed") {
    console.log(`✓ Unsubscribed from ${msg.topic}`);
    return;
  }
  if (msg.type === "error") {
    console.error(`WebSocket error: ${msg.message}`);
    return;
  }

  // Realtime event
  const { event, action, data, changedFields, receivedAt, requestId } = msg;
  console.log(`[${receivedAt}] ${event} (${action}) — requestId: ${requestId}`);

  if (event.startsWith("listing")) {
    // data.price is in cents — convert to dollars
    const dollars = data.price != null ? (data.price / 100).toFixed(2) : null;
    console.log(
      `  Product: ${data.product_id} | Ask: $${dollars} | Qty: ${data.quantity}`
    );
    if (changedFields) {
      console.log(`  Changed fields: ${changedFields.join(", ")}`);
    }
  }

  if (event.startsWith("offer")) {
    const dollars = data.price != null ? (data.price / 100).toFixed(2) : null;
    console.log(
      `  Product: ${data.product_id} | Bid: $${dollars} | Qty: ${data.quantity}`
    );
  }
});

ws.on("close", (code, reason) => {
  clearInterval(keepAlive);
  console.log(`Disconnected — code: ${code}, reason: ${reason.toString()}`);
});

ws.on("error", err => {
  console.error("WebSocket error:", err.message);
});

Python

Uses the websockets library.
pip install websockets
import asyncio
import json
import websockets

API_KEY = "YOUR_API_KEY"
WS_URL = f"wss://wscollectpure.com?apiKey={API_KEY}"


def handle_event(msg: dict) -> None:
    """Process a realtime event from the server."""
    event = msg["event"]
    action = msg["action"]
    data = msg["data"]
    changed_fields = msg.get("changedFields")
    received_at = msg["receivedAt"]
    request_id = msg["requestId"]

    print(f"[{received_at}] {event} ({action}) — requestId: {request_id}")

    if event.startswith("listing"):
        price_cents = data.get("price")
        price_str = f"${price_cents / 100:.2f}" if price_cents is not None else "spot-priced"
        print(f"  Product: {data['product_id']} | Ask: {price_str} | Qty: {data['quantity']}")
        if changed_fields:
            print(f"  Changed fields: {', '.join(changed_fields)}")

    elif event.startswith("offer"):
        price_cents = data.get("price")
        price_str = f"${price_cents / 100:.2f}" if price_cents is not None else "spot-priced"
        print(f"  Product: {data['product_id']} | Bid: {price_str} | Qty: {data['quantity']}")


async def connect() -> None:
    # ping_interval sends a WebSocket ping every 8 s to prevent the
    # server's 10-second idle timeout from closing the connection
    async with websockets.connect(
        WS_URL,
        ping_interval=8,
        ping_timeout=5,
    ) as ws:
        print("Connected to Pure WebSocket")

        # Subscribe to all marketplace listings (global)
        await ws.send(json.dumps({
            "action": "subscribe",
            "event": "listings",
        }))

        # Subscribe to offers for a specific product
        await ws.send(json.dumps({
            "action": "subscribe",
            "event": "offers",
            "filter": {"productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"},
        }))

        async for raw in ws:
            msg = json.loads(raw)

            # Control messages have a "type" field
            msg_type = msg.get("type")
            if msg_type == "subscribed":
                print(f"✓ Subscribed to {msg['topic']}")
            elif msg_type == "unsubscribed":
                print(f"✓ Unsubscribed from {msg['topic']}")
            elif msg_type == "error":
                print(f"WebSocket error: {msg['message']}")
            else:
                # Realtime event
                handle_event(msg)


if __name__ == "__main__":
    asyncio.run(connect())

Error Codes on Disconnect

The server closes connections with standard WebSocket close codes. Notable codes to handle in your client:
CodeMeaning
1000Normal closure (server or client initiated clean shutdown)
1001Server going away (deployment or restart) — reconnect with backoff
1006Abnormal closure / connection dropped — reconnect
1011Unexpected server error — reconnect with backoff
Implement exponential backoff on reconnect. Start with a 1–2 second delay and cap at 30–60 seconds.

Limitations (Alpha)

  • No snapshot on connect — subscribing does not send you the current state of the orderbook. Use the REST API (Get Product Order Book) to fetch the current book, then subscribe to the WebSocket for subsequent deltas.
  • No guaranteed delivery — if your connection drops and reconnects, events that occurred during the outage are not replayed.
  • Global streams can be high volumeglobal:listings and global:offers receive every event across the entire marketplace. For most use cases, filtering by productId is recommended.
  • Rate limits — no hard rate limits are currently enforced on the alpha, but excessive connections or message rates may result in disconnection without notice.