To receive real-time signals, send a subscription.request message after the connection is established:
{
"msg_type": "subscription.request",
"request_id": "my-sub-001",
"timestamp": "2024-01-01T12:00:00Z",
"payload": {
"action": "subscribe",
"channel": "strategy.signal",
"strategy_id": 12345678
}
}| Field | Type | Required | Description |
|---|---|---|---|
| action | string | Yes | "subscribe" or "unsubscribe" |
| channel | string | Yes | Channel to subscribe to. "strategy.signal", "strategy.live_fills" or "strategy.positions". |
| strategy_id | integer | Yes | The strategy to subscribe to. Must be a positive integer. |
| resume_from | string | No | ("strategy.signal" and "strategy.live_fills" channels only) Last seq_id received. When set and greater than 0, the server replays missed signals before starting live delivery. Omit or set to 0 to receive only signals or fills published after this subscription is acknowledged. See Gap Fill. |
| Field | Type | Required | Description |
|---|---|---|---|
| request_id | string | No | Client-assigned correlation ID (max 50 characters). The server echoes it in the subscription.ack envelope, useful when sending multiple subscriptions in quick succession. |
The timestamp field in your messages must be within 1 minute of UTC. Messages with a clock-skewed timestamp are rejected with INVALID_PARAMETER.
websocat -H "Authorization: Bearer YOUR_API_KEY" wss://api4-general.collective2.com/ws \
--text - <<< '{"msg_type":"subscription.request","timestamp":"2024-01-01T12:00:00Z","payload":{"action":"subscribe","channel":"strategy.signal","strategy_id":12345678}}'import asyncio
import json
import websockets
from datetime import datetime, timezone
API_KEY = "YOUR_API_KEY"
WS_URL = "wss://api4-general.collective2.com/ws"
STRATEGY_ID = 12345678
async def subscribe():
headers = {"Authorization": f"Bearer {API_KEY}"}
async with websockets.connect(WS_URL, additional_headers=headers) as ws:
# Wait for connection.connected
await ws.recv()
# Send subscription request
request = {
"msg_type": "subscription.request",
"request_id": "my-sub-001",
"timestamp": datetime.now(timezone.utc).isoformat(),
"payload": {
"action": "subscribe",
"channel": "strategy.signal",
"strategy_id": STRATEGY_ID
}
}
await ws.send(json.dumps(request))
# Wait for subscription.ack
ack = json.loads(await ws.recv())
print(f"Subscribed: {ack['msg_type']}")
asyncio.run(subscribe())The server responds with a subscription.ack that echoes your full payload and request_id:
{
"msg_type": "subscription.ack",
"id": "msg_a1b2c3d4e5f6a1b2c3d4e5f6a7b8",
"request_id": "my-sub-001",
"timestamp": "2024-01-01T12:00:00Z",
"v": 4,
"payload": {
"action": "subscribe",
"channel": "strategy.signal",
"strategy_id": 12345678,
"resume_from": null
}
}Once subscribed, position changes are pushed as strategy.positions messages. The message payload is PositionDTO
Once subscribed, fills are pushed as strategy.positions messages. The message payload is StrategyFillDTO
Once subscribed, new signals are pushed as strategy.signal messages:
{
"msg_type": "strategy.signal",
"id": "msg_a1b2c3d4e5f6a1b2c3d4e5f6a7b8",
"seq_id": 98765,
"timestamp": "2024-01-01T12:00:00Z",
"v": 4,
"payload": {
"Id": 98765,
"StrategyId": 12345678,
"SignalId": 456789,
"Side": "1",
"OpenClose": "O",
"OrderQuantity": 100,
"OrderType": "1",
"Stop": null,
"Limit": null,
"TIF": "Day",
"ParentSignalId": null,
"C2Symbol": {
"FullSymbol": "AAPL",
"SymbolType": "stock",
"Underlying": null,
"Expiry": null,
"PutOrCall": null,
"StrikePrice": null,
"Description": "Apple Inc."
},
"ExchangeSymbol": {
"Symbol": "AAPL",
"Currency": "USD",
"SecurityExchange": "DEFAULT",
"SecurityType": "CS",
"MaturityMonthYear": null,
"PutOrCall": null,
"StrikePrice": null,
"PriceMultiplier": 1
},
"PostedDate": "2024-01-01T12:00:00Z",
"OrderStatus": "0",
"FilledQuantity": 0,
"SignalType": 1
}
}Always store the seq_id from the last signal you receive. You will need it to resume after a disconnection — see Gap Fill.
The OrderStatus field in the payload indicates what kind of event the signal represents:
| OrderStatus` value | Meaning |
|---|---|
| "0" | Working — a new order has been placed and is active |
| "2" | Filled — the order has been fully filled |
| "4" | Canceled — the order has been canceled |
Cancel signals carry only the identity fields — Id, StrategyId, SignalId, SignalType, and OrderStatus. All order detail fields (Side, OpenClose, OrderQuantity, OrderType, Stop, Limit, TIF, C2Symbol, ExchangeSymbol, etc.) are null.
Fill signals mirror the original order signal but with OrderStatus: "2" and FilledQuantity set to the filled quantity.
Working signals are the full order structure shown in the example above.
Send a subscription.request with action: "unsubscribe" to stop receiving signals:
{
"msg_type": "subscription.request",
"timestamp": "2024-01-01T12:00:00Z",
"payload": {
"action": "unsubscribe",
"channel": "strategy.signal",
"strategy_id": 12345678
}
}A successful unsubscribe returns a subscription.ack with action: "unsubscribe" in the payload. Attempting to unsubscribe from a channel you are not subscribed to returns a NOT_FOUND error.
| Error | Cause |
|---|---|
| ALREADY_EXISTS | Already subscribed to this channel and strategy |
| NOT_FOUND | Tried to unsubscribe from a channel not currently subscribed |
| FORBIDDEN | Not authorized to receive signals for this strategy. You must own or be subscribed to the strategy to receive its signals. |
| TOO_MANY_SUBSCRIPTIONS | Channel limit reached on this connection |