Skip to content
Last updated

Subscribing to Channels

Sending a Subscription Request

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
  }
}

Payload Fields

FieldTypeRequiredDescription
actionstringYes"subscribe" or "unsubscribe"
channelstringYesChannel to subscribe to. "strategy.signal" or "strategy.positions".
strategy_idintegerYesThe strategy to subscribe to. Must be a positive integer.
resume_fromstringNo("strategy.signal" channel 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 published after this subscription is acknowledged. See Gap Fill.

Envelope Fields

FieldTypeRequiredDescription
request_idstringNoClient-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.

Using websocat

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}}'

Using Python

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())

Subscription Acknowledgement

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
  }
}

Receiving Positions

Once subscribed, position changes are pushed as strategy.positions messages. The message payload is PositionDTO

Receiving Signals

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.

Signal Types and Order Status

The OrderStatus field in the payload indicates what kind of event the signal represents:

OrderStatus` valueMeaning
"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.

Unsubscribing

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.

Subscription Errors

ErrorCause
ALREADY_EXISTSAlready subscribed to this channel and strategy
NOT_FOUNDTried to unsubscribe from a channel not currently subscribed
FORBIDDENNot authorized to receive signals for this strategy. You must own or be subscribed to the strategy to receive its signals.
TOO_MANY_SUBSCRIPTIONSChannel limit reached on this connection