> ## Documentation Index
> Fetch the complete documentation index at: https://docs.collectpure.com/llms.txt
> Use this file to discover all available pages before exploring further.

# WebSocket Streams

> Real-time orderbook data via WebSocket — subscribe to live listing and offer events

<Warning>
  **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 [support@collectpure.com](mailto:support@collectpure.com) to be
  notified of changes.
</Warning>

## 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 type | Description                                                                    |
| ---------- | ------------------------------------------------------------------------------ |
| `listings` | Sell-side orderbook — new listings, price changes, quantity changes, deletions |
| `offers`   | Buy-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:**

| Parameter    | Type      | Required | Description                                                                                                                           |
| ------------ | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `apiKey`     | `string`  | Yes      | Your Pure API key. Passed as a query parameter during the HTTP upgrade.                                                               |
| `compressed` | `boolean` | No       | Set to `true` to receive messages as zstd-compressed binary frames instead of plain text JSON. See [Compression](#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

```json theme={null}
{
  "action": "subscribe" | "unsubscribe",
  "event": "listings" | "offers",
  "filter": {
    "productId": "uuid",
    "variantId": "uuid"
  }
}
```

| Field    | Type                             | Required | Description                                                                                                                                                 |
| -------- | -------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `action` | `"subscribe"` \| `"unsubscribe"` | Yes      | Whether to start or stop receiving events for this stream                                                                                                   |
| `event`  | `"listings"` \| `"offers"`       | Yes      | The stream to subscribe or unsubscribe from                                                                                                                 |
| `filter` | `object` \| `""` \| omitted      | No       | Narrow the subscription to a specific product or variant. Omit or pass `""` for the global stream. See [Subscription Filters](#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`:

| Subscription                                            | Resolved topic                       | Receives                                                 |
| ------------------------------------------------------- | ------------------------------------ | -------------------------------------------------------- |
| `event: "listings"`, no filter                          | `global:listings`                    | All 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 filter                            | `global:offers`                      | All 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.

| Filter                     | Example                                              | Effect                                                |
| -------------------------- | ---------------------------------------------------- | ----------------------------------------------------- |
| 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 |

<Warning>
  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.
</Warning>

### Subscription Examples

```json Subscribe to all marketplace listings theme={null}
{
  "action": "subscribe",
  "event": "listings"
}
```

```json Subscribe to offers for one product theme={null}
{
  "action": "subscribe",
  "event": "offers",
  "filter": { "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" }
}
```

```json Subscribe to listings for a specific variant theme={null}
{
  "action": "subscribe",
  "event": "listings",
  "filter": {
    "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "variantId": "1e6c3d8a-8e24-4f1a-a1c2-ef2b9b3c0d91"
  }
}
```

```json Unsubscribe from a stream theme={null}
{
  "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.

```json theme={null}
{
  "type": "subscribed",
  "event": "offers",
  "filter": { "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6" },
  "topic": "offers:product:3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
```

| Field    | Type               | Description                                             |
| -------- | ------------------ | ------------------------------------------------------- |
| `type`   | `"subscribed"`     | Message type identifier                                 |
| `event`  | `string`           | The event stream you subscribed to                      |
| `filter` | `object` \| `null` | The filter applied, or `null` for a global subscription |
| `topic`  | `string`           | The resolved internal topic name                        |

### Unsubscription Confirmed

Sent immediately after a successful `unsubscribe` action.

```json theme={null}
{
  "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.

```json theme={null}
{
  "type": "error",
  "message": "Invalid action: \"ping\". Must be one of: subscribe, unsubscribe"
}
```

Common error messages:

| Message                                                | Cause                                                  |
| ------------------------------------------------------ | ------------------------------------------------------ |
| `"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.

```json theme={null}
{
  "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"
}
```

| Field                 | Type                                      | Description                                                                                                                                                                    |
| --------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `event`               | `string`                                  | Dot-notation event name, e.g. `listing.created`, `offer.updated`                                                                                                               |
| `action`              | `"created"` \| `"updated"` \| `"deleted"` | The change that occurred                                                                                                                                                       |
| `data`                | `object`                                  | The record payload. Shape depends on the event type — see [Data Schemas](#data-schemas) below                                                                                  |
| `changedFields`       | `string[]` \| `null`                      | On `updated` events, an array of the field names that changed. Always `null` for `created` and `deleted` events                                                                |
| `subscription.topic`  | `string`                                  | The internal topic that triggered this delivery                                                                                                                                |
| `subscription.filter` | `object` \| `null`                        | The filter associated with this topic, or `null` for global topics                                                                                                             |
| `receivedAt`          | `string`                                  | ISO 8601 timestamp of when the server received the upstream event                                                                                                              |
| `requestId`           | `string` (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 name        | Trigger                                                     |
| ----------------- | ----------------------------------------------------------- |
| `listing.created` | A new sell listing was posted                               |
| `listing.updated` | An existing listing changed (price, quantity, expiry, etc.) |
| `listing.deleted` | A listing was cancelled or expired                          |
| `offer.created`   | A new buy offer was placed                                  |
| `offer.updated`   | An existing offer changed                                   |
| `offer.deleted`   | An 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`.

```json theme={null}
{
  "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"
}
```

| Field                 | Type                | Description                                                                                                                 |
| --------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `id`                  | `string` (UUID)     | Unique identifier for this listing                                                                                          |
| `product_id`          | `string` (UUID)     | The product this listing is for                                                                                             |
| `variant_id`          | `string` (UUID)     | The specific variant (e.g. coin grade/size)                                                                                 |
| `price`               | `integer` \| `null` | Ask price **in cents** (e.g. `250000` = \$2,500.00). May be `null` for spot-premium priced listings                         |
| `quantity`            | `integer`           | Number of units available                                                                                                   |
| `type`                | `string`            | Pricing type: `"fixed_pricing"` or `"premium_percentage_pricing"`                                                           |
| `active`              | `boolean`           | Whether the listing is currently active. Only active listings with `quantity > 0` are emitted on `created`/`updated` events |
| `premium_percent`     | `number`            | Percentage premium over spot (e.g. `5.0` = 5%)                                                                              |
| `premium_dollar`      | `number` \| `null`  | Dollar premium over spot                                                                                                    |
| `spot_premium`        | `number` \| `null`  | Spot premium in percentage terms                                                                                            |
| `spot_premium_dollar` | `number`            | Spot premium expressed in dollars                                                                                           |
| `expires_at`          | `string` (ISO 8601) | When this listing expires                                                                                                   |
| `created_at`          | `string` (ISO 8601) | When the listing was created                                                                                                |
| `updated_at`          | `string` (ISO 8601) | When the listing was last modified                                                                                          |

<Note>
  **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.
</Note>

### Offer (`data` on offer events)

Sensitive buyer information is stripped before delivery. The following fields are **never** present: `buyer_id`, `buyer_notes`, `internal_id`.

```json theme={null}
{
  "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"
}
```

| Field                 | Type                | Description                                                          |
| --------------------- | ------------------- | -------------------------------------------------------------------- |
| `id`                  | `string` (UUID)     | Unique identifier for this offer                                     |
| `product_id`          | `string` (UUID)     | The product this offer is for                                        |
| `variant_id`          | `string` (UUID)     | The specific variant                                                 |
| `price`               | `integer` \| `null` | Bid price **in cents**. May be `null` for spot-premium priced offers |
| `quantity`            | `integer`           | Number of units the buyer wants                                      |
| `type`                | `string`            | Pricing type: `"fixed_pricing"` or `"premium_percentage_pricing"`    |
| `active`              | `boolean`           | Whether the offer is currently active                                |
| `premium_percent`     | `number`            | Percentage premium the buyer is willing to pay over spot             |
| `premium_dollar`      | `number`            | Dollar premium over spot                                             |
| `spot_premium`        | `number` \| `null`  | Spot premium in percentage terms                                     |
| `spot_premium_dollar` | `number`            | Spot premium expressed in dollars                                    |
| `expires_at`          | `string` (ISO 8601) | When this offer expires                                              |
| `created_at`          | `string` (ISO 8601) | When the offer was placed                                            |
| `updated_at`          | `string` (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:

```javascript theme={null}
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

<Note>
  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).
</Note>

***

## Code Examples

### Node.js

Uses the [`ws`](https://github.com/websockets/ws) package.

```bash theme={null}
npm install ws
```

```javascript theme={null}
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`](https://websockets.readthedocs.io/) library.

```bash theme={null}
pip install websockets
```

```python theme={null}
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:

| Code   | Meaning                                                            |
| ------ | ------------------------------------------------------------------ |
| `1000` | Normal closure (server or client initiated clean shutdown)         |
| `1001` | Server going away (deployment or restart) — reconnect with backoff |
| `1006` | Abnormal closure / connection dropped — reconnect                  |
| `1011` | Unexpected 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](/api-reference/introduction)) 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 volume** — `global: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.
