{
"type": "catalog_change",
"entity": "event",
"id": "string",
"op": "created",
"version": 0,
"changedAt": "string",
"venue": "polymarket",
"event": {
"id": "string",
"venue": "polymarket",
"externalIdentifier": "0xabc123",
"title": "string",
"slug": "string",
"status": "string",
"startDate": "string",
"endDate": "string",
"createdAt": "string",
"categories": [
{
"id": "string",
"name": "string"
}
],
"matched": true,
"clusterAnchorId": "string",
"clusterVenues": [
"string"
]
},
"market": {
"id": "string",
"venueEventId": "evt_xyz789",
"venue": "polymarket",
"externalIdentifier": "0xabc123",
"question": "string",
"shortTitle": "string",
"status": "string",
"startDate": "string",
"endDate": "string",
"createdAt": "string",
"outcomes": [
{
"id": "string",
"label": "Yes",
"title": "string"
}
],
"categories": [
{
"id": "string",
"name": "string"
}
],
"matched": true,
"clusterAnchorId": "string",
"clusterVenues": [
"string"
]
}
}{
"action": "subscribe",
"channel": "catalog",
"venues": [
"string"
],
"entities": [
"event"
],
"id": "string"
}Catalog
New, changed and deleted events and markets, one catalog_change frame per entity carrying its current state. Send { action: "subscribe", channel: "catalog", venues?, entities? }; no user auth required, and the app’s disabled venues and categories apply. Subscribing again replaces the filter.
Live only, no replay. Subscribe first, then catch up with GET /catalog/changes (same item shape) from your last changedAt minus a minute, then apply buffered frames. Apply an item when its (version, changedAt) is newer than what you hold for (entity, id); duplicates around the catch-up are expected. On stream_interrupted listing catalog, catch up over REST again.
{
"type": "catalog_change",
"entity": "event",
"id": "string",
"op": "created",
"version": 0,
"changedAt": "string",
"venue": "polymarket",
"event": {
"id": "string",
"venue": "polymarket",
"externalIdentifier": "0xabc123",
"title": "string",
"slug": "string",
"status": "string",
"startDate": "string",
"endDate": "string",
"createdAt": "string",
"categories": [
{
"id": "string",
"name": "string"
}
],
"matched": true,
"clusterAnchorId": "string",
"clusterVenues": [
"string"
]
},
"market": {
"id": "string",
"venueEventId": "evt_xyz789",
"venue": "polymarket",
"externalIdentifier": "0xabc123",
"question": "string",
"shortTitle": "string",
"status": "string",
"startDate": "string",
"endDate": "string",
"createdAt": "string",
"outcomes": [
{
"id": "string",
"label": "Yes",
"title": "string"
}
],
"categories": [
{
"id": "string",
"name": "string"
}
],
"matched": true,
"clusterAnchorId": "string",
"clusterVenues": [
"string"
]
}
}{
"action": "subscribe",
"channel": "catalog",
"venues": [
"string"
],
"entities": [
"event"
],
"id": "string"
}App-level auth: pass appId as a query parameter on connect. Required for all connections. Browsers are admitted by an Origin on the app's allowed origins; servers by the apiKey scheme instead.
Optional server-to-server auth on the handshake; any supplied key must be valid and replaces the Origin check. Without a key, an allowed Origin is required even when the app has requireApiKey for REST. WebSocket key enforcement is currently deferred. Needed for the app-events channel. Never send from a browser.
User-level auth: send { action: 'authenticate', token } after connected (a token query param still works but is deprecated). Required for user-specific events (orders, balances). Same JWT as the REST API.
One event or market's current state. op is created, updated or deleted; deletes carry only the id. Same item shape as GET /catalog/changes.
Subscribe (or unsubscribe) to new, changed and deleted events and markets