> ## Documentation Index
> Fetch the complete documentation index at: https://v3.docs.derive.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Connecting over WebSocket

> Open a session, send JSON-RPC frames, and keep the connection alive with heartbeats.

The Derive v3 API is **JSON-RPC 2.0** served over WebSocket. Every method also works
over [HTTP POST](/getting-started/introduction#endpoints), but real-time streams and
[cancel-on-disconnect](/trading/cancel-on-disconnect) are WebSocket-only.

## Connect

Open a WebSocket to the `/v3/ws` endpoint for your environment:

<CodeGroup>
  ```text Production theme={null}
  wss://api.derive.xyz/v3/ws
  ```

  ```text Testnet theme={null}
  wss://testnet.api.derive.xyz/v3/ws
  ```
</CodeGroup>

Authentication is a separate layer from the transport. You can either log in
in-band after connecting (`public/login`) or attach a read-only JWT to the URL.
See [Authentication](/authentication/session-login) for the full model.

<Tabs>
  <Tab title="In-band login (full authority)">
    Connect unauthenticated, then send `public/login` with an EIP-191 signature.
    This is the only path that can authorize trading actions.
  </Tab>

  <Tab title="JWT on the URL (read-only)">
    Pass a bearer token as a query parameter:

    ```text theme={null}
    wss://api.derive.xyz/v3/ws?token=<jwt>
    ```

    A JWT session carries only the off-chain `account_info` capability — it can
    read account data but cannot authorize actions. Action signing is still
    required for anything state-changing.
  </Tab>
</Tabs>

## Send JSON-RPC frames

Send each request as a JSON **text frame**. Responses correlate by `id`;
subscription updates arrive as separate notification frames (see
[Subscriptions](/subscriptions)).

<CodeGroup>
  ```typescript TypeScript (SDK) theme={null}
  import { DeriveClient } from '@derivexyz/derive-ts';

  // public/get_time takes no params and returns the server time in ms.
  const client = new DeriveClient({ network: 'mainnet' });
  const serverTime = await client.send('public/get_time', null);
  console.log(serverTime); // 1720483200000

  ```

  ```python Python (SDK) theme={null}
  # Coming soon.
  ```

  ```rust Rust (SDK) theme={null}
  // Coming soon.
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.derive.xyz/v3/public/get_time \
    -H "Content-Type: application/json" \
    -d '{}'
  ```

  ```json Request theme={null}
  {
    "id": 1,
    "method": "public/get_time",
    "params": {}
  }
  ```

  ```json Response theme={null}
  {
    "id": 1,
    "result": 1720483200000
  }
  ```
</CodeGroup>

<Tip>
  Assign a unique `id` per in-flight request and match responses back to it —
  frames are not guaranteed to return in send order.
</Tip>

## Heartbeat

The server drives a heartbeat loop and expects the client to stay responsive.

<Steps>
  <Step title="Server pings">
    On each heartbeat interval the server sends a WebSocket **Ping** frame. Your
    client library normally answers with a **Pong** automatically. Any inbound
    text frame from you also counts as liveness.
  </Step>

  <Step title="Application-level ping (optional)">
    You may send the text frame `ping`; the server replies with `pong`.
  </Step>

  <Step title="Miss the window and you're dropped">
    If no activity is seen within the heartbeat window, the server closes the
    connection.
  </Step>
</Steps>

The heartbeat interval is deployment-configured (currently \~180 seconds).

### Heartbeat re-authentication

On each heartbeat tick, a **logged-in** connection is silently
**re-authenticated** — the server re-runs your stored login signature check
against current account state. If the session key has expired, been revoked, or
falls outside its IP allow-list, the connection is closed. Long-lived
connections therefore pick up permission changes without you reconnecting.

## Connection limits

Concurrent WebSocket connections are capped **per source IP** (deployment-configured,
currently 4 per IP). When the cap is exceeded the server sends a text frame and
then closes the socket:

```text theme={null}
connectionLimitExceeded: <detail>
```

This maps to error code **`-32100` "Number of concurrent websocket clients limit exceeded"**.

<Info>
  The exact heartbeat window and per-IP connection cap are deployment-specific
  config values. The values above reflect current deployments; see [Rate
  limits](/rate-limits) for the per-IP connection cap and read your live budget
  from [`public/getRateLimits`](/rate-limits#runtime-introspection).
</Info>

Per-request rate limits are separate and shared across all of a wallet's
connections — see [Rate limits](/rate-limits).

## Reconnection guidance

Sessions do not survive a dropped socket. On reconnect:

<Steps>
  <Step title="Re-open the socket">
    Dial the same `/v3/ws` endpoint, with backoff (exponential + jitter) so a
    server restart doesn't produce a reconnect storm that trips the per-IP cap.
  </Step>

  <Step title="Re-authenticate">
    Re-send `public/login` (or reconnect with a fresh `?token=`). Nothing from
    the previous session — auth, subscriptions — is retained.
  </Step>

  <Step title="Re-subscribe and resync">
    Re-establish every channel from [Subscriptions](/subscriptions), then reload
    state (open orders, positions) over REST or private reads, since you may
    have missed updates while disconnected.
  </Step>
</Steps>

## Cancel on disconnect

Cancel-on-disconnect automatically cancels your resting orders, quotes, and
trigger orders when a connection drops. It is a persisted account setting — see
[Cancel on disconnect](/trading/cancel-on-disconnect) for the full behaviour and
the `private/set_cancel_on_disconnect` request.


## Related topics

- [Authenticate a WebSocket connection](/api-reference/methods/authenticate-a-websocket-connection.md)
- [Session Login](/authentication/session-login.md)
- [Subscriptions](/subscriptions.md)
