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 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 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.
{
"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 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 |
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"
}
| 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.
{
"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:
| 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.
{
"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 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.
{
"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 |
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"
}
| 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:
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.
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.
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) 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.