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

# Price Dashboard

> Build a live price dashboard on WhiteBIT public market data — a REST snapshot for the initial paint, a WebSocket stream for continuous updates, no account or API key required.

export const RegionBaseUrl = ({className = "", showBaseUrl = true, protocol = "https"}) => {
  const {useState, useEffect, useRef} = React;
  const EU_ENABLED = false;
  const [region, setRegionState] = useState(() => {
    if (!EU_ENABLED) return "com";
    if (typeof window !== 'undefined') {
      return localStorage.getItem("api-region-preference") || "com";
    }
    return "com";
  });
  const [mounted, setMounted] = useState(false);
  const observerRef = useRef(null);
  const isSyncingRef = useRef(false);
  const updateAllContentOnPage = targetRegion => {
    try {
      const domainFrom = targetRegion === "eu" ? "whitebit.com" : "whitebit.eu";
      const domainTo = targetRegion === "eu" ? "whitebit.eu" : "whitebit.com";
      const links = document.querySelectorAll('a');
      links.forEach(link => {
        let href = link.getAttribute('href');
        if (href && href.includes(domainFrom)) {
          link.setAttribute('href', href.replace(domainFrom, domainTo));
        }
      });
      const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
        acceptNode: node => {
          if (node.parentElement?.closest('.region-toggle-component')) {
            return NodeFilter.FILTER_REJECT;
          }
          return node.textContent.includes(domainFrom) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
        }
      });
      let currentNode;
      while (currentNode = walker.nextNode()) {
        currentNode.textContent = currentNode.textContent.replace(new RegExp(domainFrom, 'g'), domainTo);
      }
      console.log(`[RegionSync] Global content updated to ${domainTo}`);
    } catch (e) {
      console.error("[RegionSync] Error updating content:", e);
    }
  };
  const updateRegion = (newRegion, source) => {
    if (region === newRegion) return;
    console.log(`[RegionBaseUrl] Updating to "${newRegion}" (Source: ${source})`);
    if (source === 'observer') {
      isSyncingRef.current = true;
      setTimeout(() => isSyncingRef.current = false, 1000);
    }
    setRegionState(newRegion);
    localStorage.setItem("api-region-preference", newRegion);
    updateAllContentOnPage(newRegion);
    if (source === 'user-click') {
      window.dispatchEvent(new CustomEvent("regionChange", {
        detail: newRegion
      }));
      attemptToUpdateNativeDropdown(newRegion, 0);
      setTimeout(() => attemptToUpdateNativeDropdown(newRegion, 1), 500);
      setTimeout(() => attemptToUpdateNativeDropdown(newRegion, 2), 1500);
    }
  };
  const attemptToUpdateNativeDropdown = (targetRegion, attempt) => {
    if (isSyncingRef.current) return;
    try {
      const targetUrl = targetRegion === "eu" ? "https://whitebit.eu" : "https://whitebit.com";
      const targetDesc = targetRegion === "eu" ? "EU Server" : "Production Server";
      const selects = document.querySelectorAll('select');
      for (const select of selects) {
        if (select.innerHTML.includes('whitebit.com') || select.innerHTML.includes('whitebit.eu')) {
          select.value = targetUrl;
          select.dispatchEvent(new Event('change', {
            bubbles: true
          }));
          return;
        }
      }
      const buttons = Array.from(document.querySelectorAll('button, [role="combobox"]'));
      const serverSelector = buttons.find(btn => {
        if (btn.closest('a') || btn.closest('[class*="card"]') || btn.closest('nav')) {
          return false;
        }
        const txt = btn.textContent || "";
        const isServerDropdown = (txt.includes('Production Server') || txt.includes('EU Server') || txt.includes('WhiteBIT Global Server') || txt.includes('WhiteBIT EU Server')) && !txt.includes('Run') && !txt.includes('Send') || btn.getAttribute('role') === 'combobox';
        return isServerDropdown;
      });
      if (serverSelector) {
        const currentText = serverSelector.textContent || "";
        if (currentText.includes(targetDesc)) return;
        serverSelector.click();
        setTimeout(() => {
          const options = document.querySelectorAll('[role="option"], li, button');
          for (const opt of options) {
            const optText = opt.textContent || "";
            if (optText.includes(targetDesc) || optText.includes(targetUrl)) {
              opt.click();
              return;
            }
          }
        }, 100);
      }
    } catch (e) {
      console.error("[Sync] Error:", e);
    }
  };
  useEffect(() => {
    if (!EU_ENABLED) return;
    setMounted(true);
    updateAllContentOnPage(region);
    const handleStorageChange = e => {
      if (e.key === "api-region-preference" && e.newValue) {
        updateRegion(e.newValue, 'storage');
      }
    };
    const handleRegionChange = e => {
      if (e.detail !== region) {
        updateRegion(e.detail, 'event');
      }
    };
    window.addEventListener("storage", handleStorageChange);
    window.addEventListener("regionChange", handleRegionChange);
    observerRef.current = new MutationObserver(mutations => {
      if (isSyncingRef.current) return;
      updateAllContentOnPage(region);
      for (const mutation of mutations) {
        if (mutation.type !== 'childList' && mutation.type !== 'characterData') continue;
        const target = mutation.target;
        const el = target.nodeType === Node.TEXT_NODE ? target.parentElement : target;
        if (el && (el.getAttribute('role') === 'option' || el.closest('[role="listbox"]'))) continue;
        const text = target.textContent || "";
        if (text.includes('WhiteBIT EU Server') || text.includes('https://whitebit.eu') && text.includes('Server')) {
          if (el && el.tagName !== 'A' && !el.closest('.region-toggle-component')) {
            if (region !== 'eu') updateRegion('eu', 'observer');
          }
        } else if (text.includes('WhiteBIT Global Server') || text.includes('https://whitebit.com') && text.includes('Server')) {
          if (el && el.tagName !== 'A' && !el.closest('.region-toggle-component')) {
            if (region !== 'com') updateRegion('com', 'observer');
          }
        }
      }
    });
    observerRef.current.observe(document.body, {
      childList: true,
      subtree: true,
      characterData: true
    });
    if (typeof window !== 'undefined') {
      const current = localStorage.getItem("api-region-preference");
      if (current) attemptToUpdateNativeDropdown(current, 'init');
    }
    return () => {
      window.removeEventListener("storage", handleStorageChange);
      window.removeEventListener("regionChange", handleRegionChange);
      if (observerRef.current) observerRef.current.disconnect();
    };
  }, [region]);
  const apiBaseUrl = protocol === "wss" ? region === "eu" ? "wss://api.whitebit.eu/ws" : "wss://api.whitebit.com/ws" : region === "eu" ? "https://whitebit.eu" : "https://whitebit.com";
  const baseUrlLabel = protocol === "wss" ? "WebSocket URL" : "Base URL";
  if (!EU_ENABLED) {
    if (!showBaseUrl) return null;
    return <div className={`flex items-center gap-2 flex-wrap my-4 region-toggle-component ${className}`}>
                <span className="text-sm text-gray-500 dark:text-gray-400 font-mono">
                    {baseUrlLabel}
                </span>
                <span className="text-sm text-gray-400">:</span>
                {protocol === "wss" ? <code className="text-sm font-mono text-primary dark:text-primary-light">
                        {apiBaseUrl}
                    </code> : <a href={apiBaseUrl} target="_blank" rel="noopener noreferrer" className="text-sm font-mono text-primary dark:text-primary-light hover:underline">
                        {apiBaseUrl}
                    </a>}
            </div>;
  }
  if (!mounted) return null;
  return <div className={`flex items-center gap-2 flex-wrap my-4 region-toggle-component ${className}`}>
            <span className="text-sm text-gray-500 dark:text-gray-400 font-mono">
                {baseUrlLabel}
            </span>
            <span className="text-sm text-gray-400">(</span>
            <div className="inline-flex bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5 border border-gray-200 dark:border-gray-700">
                <button onClick={() => updateRegion("com", "user-click")} className={`px-2 py-0.5 text-xs font-medium rounded-md transition-all ${region === "com" ? "bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm" : "text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"}`}>
                    .com
                </button>
                <button onClick={() => updateRegion("eu", "user-click")} className={`px-2 py-0.5 text-xs font-medium rounded-md transition-all ${region === "eu" ? "bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm" : "text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"}`}>
                    .eu
                </button>
            </div>
            <span className="text-sm text-gray-400">)</span>
            {showBaseUrl && <>
                    <span className="text-sm text-gray-400">:</span>
                    {protocol === "wss" ? <code className="text-sm font-mono text-primary dark:text-primary-light">
                            {apiBaseUrl}
                        </code> : <a href={apiBaseUrl} target="_blank" rel="noopener noreferrer" className="text-sm font-mono text-primary dark:text-primary-light hover:underline">
                            {apiBaseUrl}
                        </a>}
                </>}
        </div>;
};

<RegionBaseUrl />

Build a live price dashboard on WhiteBIT public market data: one REST call paints the initial board, and a single WebSocket subscription keeps every displayed price current. The full flow runs without an account or API key. The guide targets price aggregators and market-data consumers.

## Prerequisites

* A terminal with curl, or any HTTP client
* Python 3 with the `websocket-client` package, or `wscat` for interactive testing — for the streaming step
* No WhiteBIT account or API key needed — every endpoint in this guide is public

## Architecture

The dashboard combines two data paths: a REST snapshot for the initial paint and a WebSocket stream for continuous updates. A REST polling loop substitutes for the stream while a dropped connection recovers.

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
    participant App as Dashboard backend
    participant REST as REST API
    participant WS as WebSocket API

    Note over App,REST: Initial paint
    App->>REST: GET /api/v4/public/markets — pair list
    App->>WS: lastprice_subscribe — tracked pairs
    App->>REST: GET /api/v4/public/ticker — price snapshot

    loop Live updates
        WS-->>App: lastprice_update — replace the displayed price
    end

    Note over App,WS: Fallback
    WS--xApp: connection drops
    App->>REST: poll GET /api/v4/public/ticker
    App->>WS: reconnect and resubscribe
```

## Select markets to track

Market discovery comes first. [`GET /api/v4/public/markets`](/api-reference/market-data/market-info) returns configuration and trading rules for every market enabled for trading. Read `name` for the pair identifier and filter by `type` (`spot`, `futures`, or `tradfiFutures`) to scope the board — a spot-only dashboard keeps `type: spot` entries. Market configuration is reference data: the server re-syncs the response approximately every 10 seconds, so fetch the list once at startup and refresh on a slow cycle.

For a step-by-step first call against each public market-data endpoint, see the [Market Data Quickstart](/products/market-data/quickstart).

## Fetch the price snapshot

[`GET /api/v4/public/ticker`](/api-reference/market-data/market-activity) returns a 24-hour pricing and volume summary for every market pair in a single response — one call fills the whole board.

<Tabs>
  <Tab title="cURL">
    ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
    curl https://whitebit.com/api/v4/public/ticker
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
    import requests

    ticker = requests.get("https://whitebit.com/api/v4/public/ticker").json()

    board = {
        pair: entry["last_price"]
        for pair, entry in ticker.items()
        if pair in ("BTC_USDT", "ETH_USDT")
    }
    print(board)
    ```
  </Tab>
</Tabs>

For Go and PHP examples, see [SDKs](/sdks).

**Response (BTC\_USDT entry, trimmed):**

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "BTC_USDT": {
    "last_price": "62741.43",
    "base_volume": "1653.886345",
    "quote_volume": "104493656.92108444",
    "isFrozen": false,
    "change": "-0.55"
  }
}
```

Key fields for a dashboard: `last_price` (most recent trade price in the quote currency), `change` (percentage change against the rolling 24-hour open), `quote_volume` (24-hour volume in the quote currency), and `isFrozen` (`true` when trading is disabled for the pair — flag or hide the entry). The API caches the response for 1 second, so polling faster than once per second returns identical data.

## Stream price updates

Polling covers one-shot lookups; a live board needs continuous updates. Subscribe to the [Last Price channel](/websocket/market-streams/lastprice) — the server pushes an update every second when the price changes, and each update replaces the displayed value. One connection carries the full watchlist: the subscription accepts an array of market names. A single connection maintains subscriptions across 200 or more markets (see [WebSocket Rate Limits](/websocket/rate-limits)).

<Tabs>
  <Tab title="Python">
    ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
    import json
    import websocket

    ws = websocket.create_connection("wss://api.whitebit.com/ws")
    ws.send(json.dumps({
        "id": 1,
        "method": "lastprice_subscribe",
        "params": ["BTC_USDT", "ETH_USDT"]
    }))
    print(ws.recv())  # subscribe confirmation

    while True:
        message = json.loads(ws.recv())
        if message.get("method") == "lastprice_update":
            market, price = message["params"]
            print(f"{market}: {price}")  # replace the displayed price
    ```
  </Tab>

  <Tab title="wscat">
    ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
    > {"id": 1, "method": "lastprice_subscribe", "params": ["BTC_USDT", "ETH_USDT"]}
    ```
  </Tab>
</Tabs>

**Subscribe confirmation:**

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"id": 1, "result": {"status": "success"}, "error": null}
```

**Update message:**

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"id": null, "method": "lastprice_update", "params": ["BTC_USDT", "62708.25"]}
```

The `params` array contains the market name (index 0) and the last traded price (index 1).

For an [order book](/glossary#order-book) widget next to the price board, add the [Depth channel](/websocket/market-streams/depth) — a snapshot-plus-delta stream with recovery rules of its own, covered in [State recovery after reconnect](/guides/websocket-quickstart#state-recovery-after-reconnect).

The [WebSocket Quickstart](/guides/websocket-quickstart) covers connection setup, keepalive pings, and the 60-second inactivity timeout; the dashboard reuses the same connection pattern.

## Reconcile snapshot and stream

Last price updates follow a replacement model: each `lastprice_update` fully replaces the previous value for the market, so the board needs no gap handling. Three rules keep the board consistent:

1. **Startup ordering.** Subscribe first, then fetch the [ticker](/glossary#ticker) snapshot. Apply snapshot values only to pairs without a streamed value yet — the snapshot response can lag behind the stream by its 1-second cache. With replacement semantics, even the reverse order self-corrects within one update interval.
2. **Liveness.** Do not infer connection health from update frequency alone. Send a `ping` every 50 seconds or less — the server closes a connection after 60 seconds of client inactivity — and treat a missed pong as a dropped connection. Reconnection with backoff and resubscription follows the standard pattern in [Reconnection and state recovery](/guides/websocket-quickstart#reconnection-and-state-recovery).
3. **Polling fallback.** While a connection recovers, keep the board live by polling [`GET /api/v4/public/ticker`](/api-reference/market-data/market-activity). The 1-second server-side cache makes one request per second the effective polling ceiling. Stop polling once resubscription confirms.

## Rate limits

The endpoints and channels used on this page carry the following limits:

| Surface                      | Limit                                           |
| ---------------------------- | ----------------------------------------------- |
| `GET /api/v4/public/markets` | 2,000 requests per 10 seconds                   |
| `GET /api/v4/public/ticker`  | 2,000 requests per 10 seconds                   |
| WebSocket connections        | 1,000 new connections per minute                |
| WebSocket requests           | 200 JSON-RPC requests per minute per connection |

Full reference: [Rate Limits & Error Codes](/api-reference/rate-limits) and [WebSocket Rate Limits & Error Codes](/websocket/rate-limits).

## Complete example

The two snippets above cover the critical path — the ticker snapshot and the price stream. The remaining assembly (multi-pair state, staleness flags, reconnection, the polling fallback) is standard application code. A complete runnable dashboard is planned as a clone-and-run starter repository.

## What's next

<CardGroup cols={3}>
  <Card title="Market Data Quickstart" icon="chart-bar" href="/products/market-data/quickstart">
    First calls against each public market-data endpoint — ping, markets, ticker, orderbook, trades.
  </Card>

  <Card title="WebSocket Quickstart" icon="bolt" href="/guides/websocket-quickstart">
    Connection setup, keepalive, reconnection, and state recovery patterns.
  </Card>

  <Card title="Partner Solutions" icon="route" href="/guides/partner-solutions">
    Integration routes by use case — trading bots, payments, account monitoring.
  </Card>
</CardGroup>
