> For the complete documentation index, see [llms.txt](https://docs.dimes.fi/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.dimes.fi/for-developers/api-and-events/websocket.md).

# WebSocket Events

> **SDK support:** The `@dimes-dot-fi/sdk/ws` entry point ships `PositionSocket` and `MarketSocket` clients for the **user gateways** (JWT auth). They wrap Socket.IO, manage reconnection, and camelize every payload to match the rest of the SDK. Prefer them over wiring up Socket.IO by hand — see [Using the SDK](#using-the-sdk) below. For the **partner gateways** (API key auth) the SDK has no client yet; use Socket.IO directly as shown in the raw examples. Runnable: [`05-websocket.ts`](https://github.com/dimes-fi/dimes-sdk/blob/main/examples/05-websocket.ts) (Node) and [`react/streams.tsx`](https://github.com/dimes-fi/dimes-sdk/blob/main/examples/react/streams.tsx) (React reconciliation).

Two WebSocket gateways are available. Both deliver the same event types and payload shapes — they differ only in authentication and scope.

| Gateway | Namespace                                  | Auth     | Scope                                     |
| ------- | ------------------------------------------ | -------- | ----------------------------------------- |
| Partner | `/v1/ws/prediction-markets/positions`      | API key  | All positions for the partner             |
| User    | `/v1/ws/prediction-markets/user-positions` | User JWT | Only positions for the authenticated user |

***

### Using the SDK

For the **user gateways**, the SDK's `@dimes-dot-fi/sdk/ws` entry point gives you typed clients so you don't touch Socket.IO directly. Both clients take a `getToken` callback (so they always reconnect with a fresh JWT) and an optional `baseUrl` (defaults to `https://api.dimes.fi` — set it to `https://api-sandbox.dimes.fi` for sandbox). Payloads arrive **camelCased** (`event.createdAt`, `event.data`) to match the REST types returned elsewhere by the SDK.

#### Position events

`PositionSocket` connects to the user-positions gateway. Subscribe to a specific event type, to `"*"` for all of them, or to `onNotification` for the user-gateway notification stream. Every `on*` call returns an unsubscribe function.

```typescript
import { PositionSocket } from "@dimes-dot-fi/sdk/ws";

const socket = new PositionSocket({
  getToken: () => currentJwt, // called on every (re)connect
  // baseUrl: "https://api-sandbox.dimes.fi", // for sandbox
});

socket.onConnect(() => console.log("connected"));

const off = socket.on("position.opened", (event) => {
  // event.data is a full Position (camelCased), same shape as the REST API
  addOpenPosition(event.data);
});

socket.on("position.opening", (event) => showFinalizing(event.data.id));

// Transient, informational messages (user gateway only)
socket.onNotification((event) => showToast(event.data.message));

socket.onError((err) => console.error("ws error", err));

socket.connect();

// later: off(); socket.disconnect();
```

When the JWT rotates, call `socket.reconnectWithToken()` to drop and re-establish the connection with the new token.

#### Market events

`MarketSocket` connects to the user-markets gateway with the same API. Remember that market event `data` is **always an array** (see [Market events](#market-events) below):

```typescript
import { MarketSocket } from "@dimes-dot-fi/sdk/ws";

const markets = new MarketSocket({ getToken: () => currentJwt });

markets.on("market.discovered", (event) => {
  for (const market of event.data) upsertMarket(market); // full Market objects
});

markets.on("market.eligibility_changed", (event) => {
  for (const delta of event.data) mergeMarketFields(delta.id, delta); // deltas
});

markets.connect();
```

The raw Socket.IO examples below remain accurate for the partner gateways and non-JS clients; the SDK clients simply wrap that same protocol for the user gateways.

***

### Partner gateway

**Namespace:**

| Environment | WebSocket URL                                                   |
| ----------- | --------------------------------------------------------------- |
| Production  | `wss://api.dimes.fi/v1/ws/prediction-markets/positions`         |
| Sandbox     | `wss://api-sandbox.dimes.fi/v1/ws/prediction-markets/positions` |

Sandbox emits the same event types and payload shapes as production — only the host and the key prefix differ. See [Environments](/for-developers/getting-started/environments.md).

**Transport:** Socket.IO

**Auth:** Partner API key, passed as either:

* **Socket.IO `auth` payload (recommended):** `auth: { apiKey: "dm_live_skey_..." }` (or `dm_sbx_skey_...` in sandbox). Transmitted in the handshake body — never appears in URLs, server logs, or browser history.
* **HTTP header:** `Authorization: Api-Key dm_live_skey_...` (or `dm_sbx_skey_...` in sandbox). For Socket.IO clients, set this via the `extraHeaders` option.

On successful auth, the connection is scoped to the partner. All events for positions created through that partner's API keys are delivered.

```javascript
import {io} from "socket.io-client";

const socket = io("wss://api.dimes.fi/v1/ws/prediction-markets/positions", {
  auth: {apiKey: process.env.DIMES_API_KEY},
});

socket.on("connect", () => {
  console.log("Connected to position events");
});

socket.on("position.opened", (event) => {
  console.log("Position opened:", event.data.id);
});

socket.on("error", (error) => {
  console.error("WebSocket error:", error);
});
```

***

### User gateway

| Environment | WebSocket URL                                                        |
| ----------- | -------------------------------------------------------------------- |
| Production  | `wss://api.dimes.fi/v1/ws/prediction-markets/user-positions`         |
| Sandbox     | `wss://api-sandbox.dimes.fi/v1/ws/prediction-markets/user-positions` |

**Transport:** Socket.IO

**Auth:** User JWT (the same token from `POST /tokens`), passed as either:

* **Socket.IO `auth` payload (recommended):** `auth: { token: "<jwt>" }`.
* **HTTP header:** `Authorization: Bearer <jwt>`. For Socket.IO clients, set this via the `extraHeaders` option.

On successful auth, the connection is scoped to the authenticated user. Only events for positions belonging to that specific wallet address and partner are delivered — other users' positions are never visible.

```javascript
import {io} from "socket.io-client";

const socket = io("wss://api.dimes.fi/v1/ws/prediction-markets/user-positions", {
  auth: {token: userJwt},
});

socket.on("position.opening", (event) => {
  // Early signal — position is being finalized on-chain
  showFinalizing(event.data.id);
});

socket.on("position.opened", (event) => {
  // Position fully open — display position card
  addOpenPosition(event.data);
});
```

***

### Authentication errors

If authentication fails on either gateway, the server emits an `error` event and disconnects:

```json
{
  "error": {
    "type": "AUTHENTICATION_ERROR",
    "code": "unauthorized",
    "message": "Unauthorized"
  }
}
```

***

### Event envelope

All events follow the `resource.action` naming pattern with a consistent envelope:

```json
{
  "id": "evt_abc123",
  "type": "position.opened",
  "created_at": "2025-06-01T10:00:05.000Z",
  "data": {
    ...
  }
}
```

| Field        | Type              | Description                                                                          |
| ------------ | ----------------- | ------------------------------------------------------------------------------------ |
| `id`         | string            | Unique event identifier (prefixed `evt_`)                                            |
| `type`       | string            | Event type in `resource.action` format                                               |
| `created_at` | string (ISO 8601) | When the event was emitted                                                           |
| `data`       | object            | Event payload — always contains the full position object matching the REST API shape |

The `data` object contains the same representation you would get from `GET /positions/{id}`. This means event consumers can reuse the same types/parsers as their REST integration.

***

### Event types

| Event Type                          | Trigger                                                                                                                                                  |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `position.created`                  | On-chain position account created (user submitted tx)                                                                                                    |
| `position.opening`                  | Finalize-open tx confirmed on-chain (early signal, before indexer picks up)                                                                              |
| `position.opened`                   | Position finalized on-chain, tokens acquired (indexer confirmed)                                                                                         |
| `position.close_requested`          | User submitted close request                                                                                                                             |
| `position.closed`                   | Close finalized, proceeds distributed                                                                                                                    |
| `position.settled`                  | Market resolved, position settled                                                                                                                        |
| `position.liquidated`               | Position liquidated due to insufficient margin                                                                                                           |
| `position.force_unwound`            | Position partially unwound to reduce leverage (position remains open)                                                                                    |
| `position.reverted`                 | Position open failed, collateral refunded                                                                                                                |
| `position.cancelled`                | Position cancelled before opening (EVM only)                                                                                                             |
| `position.settlement_state_changed` | The position's market moved to a new settlement stage (e.g. the market closed and is now awaiting resolution). The position itself does not change state |

***

### Event payloads

The `data` field in every event is a full position object — the same shape returned by the REST API. See the [Positions](https://docs.dimes.fi/for-developers/api-and-events/pages/NdOdqtZSHifVN4PbuN3l#4.-positions-user-jwt) section of the API Reference for complete field definitions.

| Event                               | `status`     | Position shape                                                                        |
| ----------------------------------- | ------------ | ------------------------------------------------------------------------------------- |
| `position.created`                  | `pending`    | Open shape (`entry`, `current`, `risk`, `fees`, `timing`)                             |
| `position.opening`                  | `pending`    | Open shape (early signal — position not yet indexed)                                  |
| `position.opened`                   | `open`       | Open shape                                                                            |
| `position.close_requested`          | `closing`    | Open shape                                                                            |
| `position.closed`                   | `closed`     | Closed shape — `close_reason: "closed"`, `entry`, `result`, `fees`                    |
| `position.settled`                  | `settled`    | Closed shape — `close_reason: "settled"`, `entry`, `result`, `fees`                   |
| `position.liquidated`               | `liquidated` | Closed shape — `close_reason: "liquidated"`, `entry`, `result`, `fees`                |
| `position.force_unwound`            | `open`       | Open shape (reduced size — `current` values reflect new position)                     |
| `position.reverted`                 | `cancelled`  | Closed shape — `close_reason: "reverted"`, `revert_reason`, `entry`, `result`, `fees` |
| `position.cancelled`                | `cancelled`  | Closed shape — `close_reason: "reverted"`, `entry`, `result`, `fees`                  |
| `position.settlement_state_changed` | unchanged    | Open shape — read `timing.settlement_state` and `timing.market_status`                |

Open positions include `current`, `risk`, `fees`, and `timing` sections. Closed, settled, and liquidated positions replace those with `close_reason` and `result` (including `collected_liquidation_fee_*` for liquidations).

The `position.reverted` payload additionally carries `revert_reason`, classifying why the open failed before it landed: `exchange_unavailable` (the prediction-market venue was temporarily unavailable — safe to retry), `slippage_exceeded` (price moved beyond your slippage tolerance before the order filled), or `unknown`. It is non-null only when `close_reason` is `reverted`; collateral is always returned regardless of reason. Surface `exchange_unavailable` as a transient, retryable issue and `slippage_exceeded` as a price-moved message.

#### `position.opening` vs `position.opened`

`position.opening` fires as soon as the finalize-open transaction is confirmed on-chain — **before** the event indexer picks it up. This gives the UI an early signal (\~5-15 seconds faster) that the position is about to open.

`position.opened` fires later when the indexer delivers the confirmed `PositionOpened` event.

**Recommended UI behavior:** On `position.opening`, show a "finalizing" state. On `position.opened`, transition to the full open position view with live data. If `position.reverted` arrives instead, the open failed — show the error state.

***

### Listening to events

Subscribe to specific event types using Socket.IO's standard event listeners:

```javascript
socket.on("position.created", (event) => {
  updatePositionStatus(event.data.id, "pending");
});

socket.on("position.opening", (event) => {
  // Early signal — show "finalizing" state
  updatePositionStatus(event.data.id, "finalizing");
});

socket.on("position.opened", (event) => {
  addOpenPosition(event.data);
});

socket.on("position.close_requested", (event) => {
  updatePositionStatus(event.data.id, "closing");
});

socket.on("position.closed", (event) => {
  showSettlement(event.data);
});

socket.on("position.settled", (event) => {
  showSettlement(event.data);
});

socket.on("position.liquidated", (event) => {
  alertLiquidation(event.data);
});

socket.on("position.force_unwound", (event) => {
  // Position still open but reduced in size
  updateOpenPosition(event.data);
});

socket.on("position.reverted", (event) => {
  // Open failed — show error
  showOpenFailed(event.data);
});

socket.on("position.cancelled", (event) => {
  showCancelled(event.data);
});
```

***

### Notification events (user gateway only)

In addition to position state-transition events, the user gateway emits `notification` events. These are informational messages that provide feedback during asynchronous operations — they are not errors and do not indicate a state change.

**Envelope:**

```json
{
  "id": "evt_abc123",
  "type": "notification",
  "created_at": "2025-06-01T10:00:05.000Z",
  "data": {
    "code": "ORDER_FULFILLMENT_RETRYING",
    "message": "Order fulfillment not possible at current slippage tolerance. Retrying.",
    "params": {
      "position_id": "dm_pos_abc123",
      "slippage_bps": 150
    }
  }
}
```

| Field          | Type   | Description                                      |
| -------------- | ------ | ------------------------------------------------ |
| `data.code`    | string | Machine-readable notification code               |
| `data.message` | string | Human-readable description                       |
| `data.params`  | object | Additional context (varies by notification code) |

**Notification codes:**

| Code                         | When                                                                            | Params                                                                                     |
| ---------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `ORDER_FULFILLMENT_RETRYING` | Order could not be filled at current slippage; retrying                         | `position_id`, `slippage_bps`                                                              |
| `PARTIAL_OPEN_PROGRESS`      | Floor or retry leg of a FAK partial-open fulfilled; reports cumulative progress | `position_id`, `cumulative_filled_making_amount`, `target_making_amount`, `attempt_number` |
| `PARTIAL_OPEN_FLOOR_MISSED`  | Floor FOK of a FAK partial-open failed to fill after retries; position reverted | `position_id`                                                                              |

**Listening:**

```javascript
socket.on("notification", (event) => {
  showToast(event.data.message);
});
```

Notifications are transient — display them as a toast or inline status message. They do not require user action.

***

## Market events

In addition to position events, two WebSocket gateways stream **market** updates: new markets coming online, eligibility changes, and max-leverage changes. They mirror the position gateways exactly — same Socket.IO transport, same auth, same envelope — and differ only in scope.

| Gateway | Namespace                                | Auth     | Scope                                     |
| ------- | ---------------------------------------- | -------- | ----------------------------------------- |
| Partner | `/v1/ws/prediction-markets/markets`      | API key  | All markets visible to the partner        |
| User    | `/v1/ws/prediction-markets/user-markets` | User JWT | Markets visible to the authenticated user |

Auth is identical to the position gateways: the partner gateway takes an API key (`auth: { apiKey: "dm_live_skey_..." }` or `Authorization: Api-Key ...`), and the user gateway takes a user JWT (`auth: { token: "<jwt>" }` or `Authorization: Bearer <jwt>`). See [Partner gateway](#partner-gateway) and [User gateway](#user-gateway) above for the full handshake details — they apply unchanged.

| Environment | Partner URL                                                   | User URL                                                           |
| ----------- | ------------------------------------------------------------- | ------------------------------------------------------------------ |
| Production  | `wss://api.dimes.fi/v1/ws/prediction-markets/markets`         | `wss://api.dimes.fi/v1/ws/prediction-markets/user-markets`         |
| Sandbox     | `wss://api-sandbox.dimes.fi/v1/ws/prediction-markets/markets` | `wss://api-sandbox.dimes.fi/v1/ws/prediction-markets/user-markets` |

### `data` is always an array

> **Important:** Unlike position events — where `data` is a single object — market events always deliver `data` as an **array**. A single market change arrives as an array of one; when many markets change at once (e.g. a batch eligibility sweep), they are batched into a single event. **Always iterate `data` as an array.**

The envelope is otherwise identical to position events:

```json
{
  "id": "evt_mkt_abc123",
  "type": "market.discovered",
  "created_at": "2025-06-01T10:00:05.000Z",
  "data": [
    { ... },
    { ... }
  ]
}
```

### Market event types

| Event Type                    | Trigger                                                     | `data` items                                                                 |
| ----------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `market.discovered`           | A new market comes online                                   | **Full** market objects (same shape as `GET /v1/markets`)                    |
| `market.eligibility_changed`  | A market's tradeability / capacity / notional limits change | **Delta** objects — only the changed fields                                  |
| `market.max_leverage_changed` | An eligible market's max leverage changes                   | **Delta** objects — `id`, `polymarket`, `leverage` (no `min_bps`/`step_bps`) |

`market.discovered` carries the **complete** market payload — identical to an item from `GET /v1/markets`, including the optional `prices` block when available. The two `*_changed` events carry **deltas**: each item contains the market `id`, the always-present `polymarket` identifiers (so you can map the delta onto your cached market), and **only the fields that actually changed**. Merge those fields into your cached copy of the market by `id`.

#### `market.discovered`

`data` is an array of full market objects, each identical to an item from `GET /v1/markets`. See the [Markets](/for-developers/api-and-events/api-reference.md) section of the API Reference for complete field definitions.

```json
{
  "id": "evt_mkt_abc123",
  "type": "market.discovered",
  "created_at": "2025-06-01T10:00:05.000Z",
  "data": [
    {
      "id": "dm_mkt_abc123",
      "category": "politics",
      "provider": "polymarket",
      "status": "active",
      "ticker": "will-trump-win-the-2024-election",
      "title": "Will Trump win the 2024 election?",
      "accepting_new_positions": true,
      "min_notional_usd": "15.00",
      "min_notional_usd_pips": "150000",
      "max_notional_yes_usd": "50.00",
      "max_notional_no_usd": "50.00",
      "sided_eligibility": { "yes": { "...": "..." }, "no": { "...": "..." } },
      "leverage": { "...": "..." },
      "fees": { "...": "..." },
      "polymarket": {
        "condition_id": "0xabc123...",
        "no_token_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
        "slug": "will-trump-win-the-2024-election",
        "yes_token_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455"
      },
      "prices": {
        "yes_bid_price_usd": "0.49",
        "yes_ask_price_usd": "0.51",
        "no_bid_price_usd": "0.49",
        "no_ask_price_usd": "0.51"
      }
    }
  ]
}
```

#### `market.eligibility_changed`

`data` is an array of deltas. Each delta is `{ id, polymarket, ...only the changed fields }`. Changed fields may include any of: `accepting_new_positions`, `rejection_reason_code`, `sided_eligibility`, `max_notional_yes_usd(_pips)`, `max_notional_no_usd(_pips)`, `capacity_max_notional_yes_usd(_pips)`, `capacity_max_notional_no_usd(_pips)`, `slippage_max_notional_yes_usd(_pips)`, `slippage_max_notional_no_usd(_pips)`. It **never** includes `leverage`.

```json
{
  "id": "evt_mkt_def456",
  "type": "market.eligibility_changed",
  "created_at": "2025-06-01T10:01:00.000Z",
  "data": [
    {
      "id": "dm_mkt_abc123",
      "polymarket": {
        "condition_id": "0xabc123...",
        "no_token_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
        "slug": "will-trump-win-the-2024-election",
        "yes_token_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455"
      },
      "accepting_new_positions": false,
      "rejection_reason_code": "QUOTE_TRADING_WINDOW_CLOSING"
    }
  ]
}
```

#### `market.max_leverage_changed`

`data` is an array of deltas. Each delta is `{ id, polymarket, leverage }`, where `leverage` carries the same fields as the `leverage` block in a full market object **except** the constants `min_bps` and `step_bps` — those never change, so read them from the market discovery payload. Only emitted for markets that are currently allowed/eligible.

```json
{
  "id": "evt_mkt_ghi789",
  "type": "market.max_leverage_changed",
  "created_at": "2025-06-01T10:02:00.000Z",
  "data": [
    {
      "id": "dm_mkt_abc123",
      "polymarket": {
        "condition_id": "0xabc123...",
        "no_token_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
        "slug": "will-trump-win-the-2024-election",
        "yes_token_id": "21742633143463906290569050155826241533067272736897614950488156847949938836455"
      },
      "leverage": {
        "max_bps": 50000,
        "max_yes_bps": 50000,
        "max_no_bps": 30000,
        "max_market_leverage_per_notional": {
          "yes": { "at100_usd_bps": 50000, "at500_usd_bps": 40000, "at1000_usd_bps": 30000, "at10000_usd_bps": 20000 },
          "no": { "at100_usd_bps": 30000, "at500_usd_bps": 25000, "at1000_usd_bps": 20000, "at10000_usd_bps": 15000 }
        }
      }
    }
  ]
}
```

### Connecting to market events

The connection flow is identical to the position gateways — only the namespace changes. Remember to iterate `data`:

```javascript
import {io} from "socket.io-client";

const socket = io("wss://api.dimes.fi/v1/ws/prediction-markets/user-markets", {
  auth: {token: userJwt},
});

socket.on("connect", () => {
  console.log("Connected to market events");
});

socket.on("market.discovered", (event) => {
  // Full market objects — add/refresh them in your local store
  for (const market of event.data) {
    upsertMarket(market);
  }
});

socket.on("market.eligibility_changed", (event) => {
  // Deltas — merge only the changed fields into the cached market by id
  for (const delta of event.data) {
    mergeMarketFields(delta.id, delta);
  }
});

socket.on("market.max_leverage_changed", (event) => {
  for (const delta of event.data) {
    mergeMarketFields(delta.id, {leverage: delta.leverage});
  }
});

socket.on("error", (error) => {
  console.error("WebSocket error:", error);
});
```

For the partner gateway, swap the namespace to `/v1/ws/prediction-markets/markets` and authenticate with your API key (`auth: {apiKey: process.env.DIMES_API_KEY}`) exactly as shown for the [partner position gateway](#partner-gateway).

***

## Testing your integration

Waiting for real positions to move through their lifecycle is a slow way to build a UI. `POST /sample-events` emits **one of every event type** onto your own WebSocket connection, one per second, with fixed mock payloads — so you can build and debug every branch of your event handling in under a minute.

|                  |                                             |
| ---------------- | ------------------------------------------- |
| **Endpoint**     | `POST /v1/prediction-markets/sample-events` |
| **Auth**         | Partner API key **or** user JWT             |
| **Rate limit**   | One request every 30 seconds                |
| **Availability** | All environments, including production      |

Which gateway receives the events depends on how you authenticate:

* **Partner API key** → the events land on the [partner positions gateway](#partner-gateway) (`/v1/ws/prediction-markets/positions`), scoped to your partner. You get every position event type.
* **User JWT** → the events land on the [user positions gateway](#user-gateway) (`/v1/ws/prediction-markets/user-positions`), scoped to the wallet in the token. You get every position event type **plus** one `notification` event for every notification code.

Only your own connection receives these events. Nobody else's clients see it.

**Nothing is created or persisted.** The payloads are canned mock data: every one carries the position ID `dm_pos_sampleevent000000` and the ticker `dimes-sample-market`, neither of which exists. Fetching them over REST will 404 — that is expected. Filter sample events out of production analytics by checking for that position ID if you need to.

### Connect first, then call

The events are fired at whatever is connected *at that moment*. Open your WebSocket, wait for it to authenticate, and only then call the endpoint.

```bash
# Partner scope
curl -X POST https://api.dimes.fi/v1/prediction-markets/sample-events \
  -H "Authorization: Api-Key $DIMES_API_KEY"

# User scope
curl -X POST https://api.dimes.fi/v1/prediction-markets/sample-events \
  -H "Authorization: Bearer $USER_JWT"
```

The call returns `202 Accepted` immediately with the size and pacing of what is about to arrive:

```json
{
  "event_count": 19,
  "interval_ms": 1000
}
```

The position events arrive first, then the notification events. Read `event_count` to know how many messages to expect and `interval_ms` for the spacing — do not hard-code either. New event types are added to the sample set as they are added to the protocol, so a handler that copes with everything listed under [Event types](#event-types) will not be surprised later.

### A worked example

```javascript
import {io} from "socket.io-client";

const socket = io("wss://api.dimes.fi/v1/ws/prediction-markets/user-positions", {
  auth: {token: userJwt},
});

socket.onAny((type, event) => {
  console.log(type, event.data.status ?? event.data.code);
});

socket.on("connect", async () => {
  await fetch("https://api.dimes.fi/v1/prediction-markets/sample-events", {
    method: "POST",
    headers: {authorization: `Bearer ${userJwt}`},
  });
});
```

### What the sample events do not cover

* **Market events.** `market.discovered`, `market.eligibility_changed` and `market.max_leverage_changed` are global broadcasts, so they are deliberately excluded. Sandbox produces real market events at a steady rate — use it to exercise that path.
* **Ordering and timing realism.** The endpoint walks the whole catalogue in a fixed order in one pass. Real positions emit a handful of these, with gaps of seconds to days between them, and occasionally out of order.
* **Error paths.** Authentication failures, disconnects and reconnection are not simulated. Test those by dropping the connection yourself.

***

### Best practices

**Deduplicate on event `id`.** Store processed event IDs and skip duplicates. The same event may be delivered more than once.

**Handle events out of order.** A `position.closed` event could arrive before `position.close_requested` due to network timing. Use the `created_at` timestamp and the position status from the REST API as the source of truth if ordering matters.

**Never assume you know every event type.** New event types are added over time and will start arriving on your existing connection without warning. Route on the `type` field with an explicit default branch that logs and ignores anything unrecognised — never one that throws or drops the connection.

**Treat `data.status` as the truth, not the event name.** Several events leave a position `open` (`position.force_unwound`). Re-render from the payload's `status` and `current` values rather than inferring state from which event fired.

**Reconnect on disconnect.** Configure your client's reconnection settings for production reliability.

**Use REST API as fallback.** If your WebSocket connection drops, use `GET /positions` to catch up on any missed state changes. Events are a latency optimisation over polling, not a durable log — there is no replay of missed events, so a reconnect should always be followed by a REST reconciliation.

**Build against `POST /sample-events` first.** Wire up every branch of your handler against the mock events before you place a single real position — see [Testing your integration](#testing-your-integration) above.
