# Subscribing to Channels

## Sending a Subscription Request

To receive real-time signals, send a `subscription.request` message after the connection
is established:


```json
{
  "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

| Field | Type | Required | Description |
|  --- | --- | --- | --- |
| action | string | Yes | `"subscribe"` or `"unsubscribe"` |
| channel | string | Yes | Channel to subscribe to. `"strategy.signal"` or `"strategy.positions"`. |
| strategy_id | integer | Yes | The strategy to subscribe to. Must be a positive integer. |
| resume_from | string | No | (`"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](/guides/websocket-gap-fill). |


### Envelope Fields

| 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`.

### Using websocat


```bash
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


```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`:


```json
{
  "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:


```json
{
  "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](/guides/websocket-gap-fill).

## Signal Types and Order Status

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.

## Unsubscribing

Send a `subscription.request` with `action: "unsubscribe"` to stop receiving signals:


```json
{
  "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

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