# WebSocket messages

All messages are JSON text frames.

## Client → server

| Action | Example | Purpose |
| --- | --- | --- |
| `auth` | `{"action":"auth","api_key":"mna-live-..."}` | Authenticate if you didn't pass `api_key` in the URL. |
| `subscribe` | `{"action":"subscribe","channels":["ticker:AAPL"]}` | Add channels. |
| `unsubscribe` | `{"action":"unsubscribe","channels":["ticker:AAPL"]}` | Remove channels. Use `["*"]` to remove all. |
| `list` | `{"action":"list"}` | Get your current subscriptions. |
| `ping` | `{"action":"ping"}` | Application-level ping. The server replies with `pong`. |

## Server → client

| Type | When |
| --- | --- |
| `welcome` | After successful authentication. `stream` is `immediate` or `summaries`, and `message` says whether this socket waits for the summary. |
| `subscribed` / `unsubscribed` | Confirms a subscription change, with the channels changed and your new total. |
| `subscriptions` | Reply to `list`. |
| `article` | A new article matching your channels. `matched` lists the channels it matched. |
| `heartbeat` | Every 30 seconds, so you can detect dead connections. |
| `pong` | Reply to `ping`. |
| `error` | A non-fatal problem with your last message. The connection stays open. |

### article

```json
{
 "type": "article",
 "matched": [
 "ticker:AAPL"
 ],
 "data": {
 "id": "art_9f2c1d7e4b8a6035",
 "title": "Apple unveils new on-device AI features, shares rise in premarket trading",
 "url": "https://www.example-publisher.com/markets/apple-ai-features",
 "source": {
 "name": "Example Publisher",
 "domain": "example-publisher.com"
 },
 "publisher": "Example Publisher",
 "published_at": "2026-10-06T12:31:04Z",
 "ingested_at": "2026-10-06T12:31:09Z",
 "category": "stocks",
 "type": "news",
 "tickers": [
 "AAPL"
 ],
 "listings": [
 {
 "symbol": "AAPL",
 "exchange": "NASDAQ"
 }
 ],
 "snippet": "Apple on Tuesday introduced a set of on-device AI features for iPhone and Mac, sending the stock higher before the open.",
 "summary": "Apple announced new on-device AI features for iPhone and Mac. Shares rose about 2% in premarket trading as analysts called the update a catalyst for the upgrade cycle.",
 "body": null,
 "entities": [
 {
 "type": "company",
 "name": "Apple Inc.",
 "symbol": "AAPL"
 },
 {
 "type": "product",
 "name": "iPhone"
 }
 ],
 "language": "en"
 }
}
```

### heartbeat

```json
{
 "type": "heartbeat",
 "time": "2026-10-06T12:31:30Z"
}
```

### error

```json
{
 "type": "error",
 "code": "invalid_channel",
 "message": "Unknown channel prefix 'tickr'. See /docs/websocket-channels."
}
```

| Error code | Meaning |
| --- | --- |
| `invalid_message` | Not valid JSON, or missing `action`. |
| `unknown_action` | The `action` isn't one listed above. |
| `invalid_channel` | Malformed channel or unknown prefix. |
| `too_many_subscriptions` | The subscribe would exceed 200 channels. Nothing was added. |
| `not_authenticated` | You sent a command before authenticating. |

## Close codes

| Code | Meaning |
| --- | --- |
| `1000` | Normal close (you or the server closed cleanly). |
| `1001` | Server restart during a deploy. Reconnect immediately. |
| `4001` | Missing or invalid API key. |
| `4003` | Your plan doesn't include the WebSocket stream (Pro required). |
| `4008` | Too many concurrent connections for your account (3 on Stream). |
| `4009` | Authentication timed out: no key in the URL and no `auth` message within 10 seconds. |
| `4029` | You sent more than 20 control messages per second. |
| `4030` | Your client fell too far behind (send buffer full). Read faster or narrow your subscriptions. |

Reconnect on `1001`, `1006` and `4030`. Don't auto-reconnect on `4001` or `4003`, because the same key will fail again.
