# MEXC WebSocket API in Python: protobuf and Blocked errors

URL: https://fincodelabs.com/en/blog/mexc-websocket-python · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Exchange APIs · Published: 2026-11-06 · Updated: 2026-11-06

MEXC WebSocket in Python, tested 5 November 2026: why subscriptions return Blocked, compiling the protobuf files, decoding streams and depth versions.

**Short answer:** The **MEXC WebSocket** at `wss://wbs-api.mexc.com/ws` sends spot market data as protobuf, and on 5 November 2026 it answered old JSON channel names with Reason: Blocked while still returning code 0. In Python, compile MEXC's 16 `.proto` files with grpcio-tools, subscribe to channels such as `spot@public.aggre.deals.v3.api.pb@100ms@BTCUSDT`, and decode each binary frame as `PushDataV3ApiWrapper`. No test environment was found in the official documentation read that day.

Sources: MEXC's official spot v3 documentation at mexc.com/api-docs (change log to 23 October 2026), read on 5 November 2026, and tests that day from 19:10 to 20:20 UTC with Python 3.11.3, websockets 17.4 and protobuf 7.36.2, on public data only.

This is a technical guide, not a recommendation of MEXC or any strategy; automated trading can lose money, and access depends on your jurisdiction and the platform's terms. Article 99/A of Capital Markets Law No. 6362 treats activity by foreign crypto-asset platforms directed at Turkish residents as unauthorized. The Capital Markets Board (SPK) list of crypto-asset service providers in operation includes Mexc Kripto Varlık Alım Satım Platformu A.Ş.; this page is not about that company, and SPK says listing does not mean authorization.

## MEXC WebSocket Blocked: what the error means

Blocked means the server refused the subscription; in these tests it was not an IP ban, because the same connection subscribed to protobuf channels seconds later.

The refusal is JSON text with `code` 0, like a success, so a client checking only `code` sees no error. A refusal, minus its two exclamation marks: `{"id":7,"code":0,"msg":"Not Subscribed successfully [spot@public.deals.v3.api@BTCUSDT]. Reason： Blocked"}`.

**Subscription replies from wss://wbs-api.mexc.com/ws, 5 November 2026**

| Channel sent | Reply |
| --- | --- |
| JSON-era names without `.pb`, such as `spot@public.deals.v3.api@BTCUSDT` | Blocked |
| `spot@public.deals.v3.api.pb@BTCUSDT`, not aggregated | Blocked |
| Lowercase symbol, 50ms interval or unknown symbol | Blocked |
| Any channel on the old host `wss://wbs.mexc.com/ws` | Blocked |
| `spot@public.aggre.deals.v3.api.pb@100ms@BTCUSDT` | The channel name, then binary frames |
| Same channel twice | An empty `msg` |
| 31st channel on one connection | Exceeded maximum subscription limit |

Also accepted: 10ms deals, aggregated depth and book ticker, 5-level limit depth and klines (`spot@public.kline.v3.api.pb@BTCUSDT@Min1`). Two channels in one request got one reply listing both, not always in the order sent, so match replies by the echoed `id` (PONG always carries id 0). MEXC's error code page lists about 94 codes, none for Blocked.

## MEXC WebSocket documentation: limits as of November 2026

Every documented rule that was tested held.

**Documented and observed, 5 November 2026**

| Rule | Documented | Observed |
| --- | --- | --- |
| Connection life | At most 24 hours | Not tested |
| No valid subscription | Closed after 30 seconds | Closed about 34 seconds after connecting, with no close status code |
| Subscribed, no data | Closed after one minute | Not tested |
| Streams per connection | 30 | The 31st was refused |
| Requests | 100 per second; connections over it are closed, and IPs closed repeatedly may be blocked | Not tested |
| Ping | Client sends `{"method": "PING"}`; the FAQ also mentions server pings | PING and ping both got PONG; no server ping in 75 seconds |

Use mexc.com/api-docs: an older official copy at mexcdevelop.github.io, with a change log ending 24 February 2025, still shows a `ws://` endpoint (it redirected to https) and a 1,000-level snapshot.

## MEXC protobuf in Python: compile the 16 .proto files

Download MEXC's websocket-proto repository, compile its 16 files with grpcio-tools, and put the output folder on the import path.

```bash
git clone https://github.com/mexcdevelop/websocket-proto
python -m pip install grpcio-tools protobuf websockets requests
mkdir gen
python -m grpc_tools.protoc -I websocket-proto --python_out=gen websocket-proto/*.proto
```

These steps ran on 5 November 2026 in a fresh virtual environment; the repository (Apache 2.0) was last updated 9 September 2026. The command on MEXC's page, `protoc *.proto --python_out=python custom_path`, failed as written on its `custom_path` argument. Three traps:

- **Imports and versions.** The files declare no package, so generated modules import each other by bare name: add `gen` to `sys.path`. Code generated as protobuf 7.35.1 failed with `VersionError` under runtime 7.34.1: the runtime may not be older.
- **The official demo.** MEXC's Python WebSocket example (mexcdevelop/mexc-api-demo) ships files generated with protobuf 5.29.2, without the current `tradeId` field, disables TLS certificate checks (`sslopt={"cert_reqs": 0}`) and ignores refusals. Regenerate rather than copy.
- **Fields.** On aggregated deals, `createTime` came back 0 and `symbolId` empty, while `tradeId` held strings such as `747104443186495488X1_747104442330845184X3`. Prices are strings: parse them with `Decimal`, not `float`.

## MEXC WebSocket API Python example, tested

This client subscribes one channel per request, classifies every reply, decodes binary frames, checks depth versions, pings every 20 seconds and reconnects with backoff.

```python
import asyncio, itertools, json, random, re, sys, time
from decimal import Decimal
import requests, websockets
sys.path.insert(0, "gen")              # output folder of grpc_tools.protoc
import PushDataV3ApiWrapper_pb2 as pb

URL = "wss://wbs-api.mexc.com/ws"
REFUSED = re.compile(r"Not Subscribed successfully\W+\[([^\]]*)\]\W*Reason\W*(\w[\w ]*\w)")

def check_ack(params, msg):            # code is 0 even on a refusal: read msg
    if m := REFUSED.search(msg):
        return f"refused [{m.group(1)}]: {m.group(2)}"
    if msg == "":
        return "empty reply: duplicate subscription?"
    return "ok" if set(msg.split(",")) == set(params) else f"unexpected: {msg}"

def num(text):                         # exact; empty, NaN and Infinity rejected
    d = Decimal(text or "NaN")
    if not d.is_finite():
        raise ValueError(f"bad number {repr(text)}")
    return d

def in_order(prev_to, frm, to):        # fromVersion = previous toVersion + 1
    return bool(frm and to) and (prev_to is None or int(frm) == prev_to + 1)

def align(buffered, last_update_id):   # diffs to apply, [] to wait, None to refetch
    rest = [d for d in buffered if int(d.toVersion) > last_update_id]
    if rest and int(rest[0].fromVersion) > last_update_id + 1:
        return None
    return rest

def snapshot(symbol):
    r = requests.get("https://api.mexc.com/api/v3/depth", timeout=10,
                     params={"symbol": symbol, "limit": 5000})
    if not r.ok:                       # status first, then the body
        raise RuntimeError(f"snapshot failed: HTTP {r.status_code} {r.text[:80]}")
    body = r.json()
    if "lastUpdateId" not in body:
        raise RuntimeError(f"snapshot failed: {body.get('code')} {body.get('msg')}")
    return body

async def stream(channels, seconds):
    ids, pending, trades, prev_to, buffered, snap = itertools.count(1), {}, 0, None, [], None
    async with websockets.connect(URL, ping_interval=None, max_size=2**22) as ws:
        for ch in channels:            # one channel per request; ids from 1, PONG uses 0
            pending[i := next(ids)] = [ch]
            await ws.send(json.dumps({"method": "SUBSCRIPTION", "params": [ch], "id": i}))
        end = last_rx = next_ping = time.monotonic()
        end += seconds
        while (now := time.monotonic()) < end:
            if now - last_rx > 60:
                raise TimeoutError("no frames for 60 s")
            if now >= next_ping:
                await ws.send(json.dumps({"method": "PING"}))
                next_ping = now + 20
            try:
                frame = await asyncio.wait_for(ws.recv(), timeout=next_ping - now)
            except TimeoutError:
                continue
            last_rx = time.monotonic()
            if isinstance(frame, str):
                reply = json.loads(frame)
                params = pending.pop(reply.get("id"), None)
                print(f"ack {params[0]} -> {check_ack(params, reply.get('msg', ''))}" if params else f"text {frame}")
                continue
            w = pb.PushDataV3ApiWrapper()
            w.ParseFromString(frame)
            kind = w.WhichOneof("body")
            if kind == "publicAggreDeals" and trades < 2:
                for d in w.publicAggreDeals.deals[:1]:
                    trades += 1
                    print("trade", w.symbol, {1: "buy", 2: "sell"}.get(d.tradeType, d.tradeType),
                          num(d.price), num(d.quantity), d.time)
            elif kind == "publicAggreDepths":
                d = w.publicAggreDepths
                if not in_order(prev_to, d.fromVersion, d.toVersion):
                    print("gap or missing version: rebuild from a new snapshot")
                    buffered, snap = [], None
                prev_to = int(d.toVersion) if d.toVersion else None
                if buffered is not None and d.fromVersion and d.toVersion:
                    buffered.append(d)
                    if snap is None and len(buffered) >= 3:
                        snap = await asyncio.to_thread(snapshot, w.symbol)
                    rest = align(buffered, snap["lastUpdateId"]) if snap else []
                    if rest is None:
                        snap, buffered = None, []
                    elif rest:
                        print("snapshot", snap["lastUpdateId"], "levels", len(snap["bids"]),
                              "apply from", rest[0].fromVersion)
                        buffered = None        # a real book applies rest, then each diff
        print("depth in order up to version", prev_to)

async def run(channels, seconds, sessions=1):  # production: sessions=None, seconds < 86400
    delay, done = 1, 0
    while sessions is None or done < sessions:
        try:
            await stream(channels, seconds)
            delay, done = 1, done + 1
        except (OSError, TimeoutError, websockets.WebSocketException) as err:
            print("reconnect after", type(err).__name__)
            await asyncio.sleep(delay + random.random())
            delay = min(delay * 2, 60)

if __name__ == "__main__":
    asyncio.run(run(["spot@public.aggre.deals.v3.api.pb@100ms@BTCUSDT",
                     "spot@public.aggre.depth.v3.api.pb@100ms@BTCUSDT",
                     "spot@public.deals.v3.api@BTCUSDT"], seconds=25))
```

Output at 19:37 UTC on 5 November 2026:

```
ack spot@public.aggre.deals.v3.api.pb@100ms@BTCUSDT -> ok
ack spot@public.aggre.depth.v3.api.pb@100ms@BTCUSDT -> ok
ack spot@public.deals.v3.api@BTCUSDT -> refused [spot@public.deals.v3.api@BTCUSDT]: Blocked
text {"id":0,"code":0,"msg":"PONG"}
trade BTCUSDT sell 76141 0.00006895 1793907398598
snapshot 85568364113 levels 2000 apply from 85568364109
trade BTCUSDT buy 76141.01 0.0003514 1793907399098
text {"id":0,"code":0,"msg":"PONG"}
depth in order up to version 85568402665
```

Failure paths were tested offline that day: captured replies through `check_ack`; `NaN`, `Infinity` and empty fields rejected by `num`, `1e400` kept exact; a local mock server sending a version gap, a frame without versions and a stale snapshot, each followed by a rebuild; and a closed port for the backoff. Watchdog design is in [WebSocket heartbeat and reconnect](https://fincodelabs.com/en/blog/websocket-heartbeat-reconnect).

## MEXC WebSocket order book: versions and the snapshot

MEXC's book procedure is the usual snapshot plus diffs, keyed on `fromVersion` and `toVersion`.

1. Subscribe to the aggregated depth channel and buffer diffs.
2. Fetch `GET /api/v3/depth?symbol=SYMBOL&limit=5000`; keep `lastUpdateId`.
3. Drop diffs whose `toVersion` is not above it; if the first remaining diff starts above `lastUpdateId + 1`, fetch again.
4. Apply in order; each `fromVersion` must equal the previous `toVersion` + 1, or rebuild. Quantity 0 removes a level.

> **Test, 5 November 2026.** Buffered diffs covered versions 85565872256–85565872292, 85565872293–85565872330 and 85565872331–85565872436. The snapshot's `lastUpdateId` was 85565872362, so the first two were dropped and the third applied. The next 55 diffs, over 6 seconds, had no gap.

If the snapshot is newer than every buffered diff, keep buffering instead of fetching again.

**Snapshots stopped at 2,000 levels.** The documented maximum `limit` is 5000, at weight 3. At 19:14 and 20:12 UTC on 5 November 2026, limits of 2001, 3000 and 5000 all returned 2,000 bids and 2,000 asks for BTCUSDT and ETHUSDT; deeper levels enter the book only when they change. Generic book code is in [local order book via WebSocket](https://fincodelabs.com/en/blog/websocket-order-book).

## MEXC API Python: signing REST requests

Signed REST calls send the key in an `X-MEXC-APIKEY` header and a lowercase hex HMAC SHA256 of the parameters, including a millisecond `timestamp`.

As documented on 5 November 2026, the signed string is the query string followed by the body with no `&` between; `recvWindow` may not exceed 60 seconds. REST limits: 300 weight per 10 seconds per IP, 500 per UID; ignoring HTTP 429 earns IP bans of 2 minutes to 3 days.

```python
import hashlib, hmac, urllib.parse

def sign(secret, params):                    # params include timestamp and recvWindow
    if not all(isinstance(v, (str, int)) for v in params.values()):
        raise TypeError("pass prices and quantities as strings")
    total = urllib.parse.urlencode(params)   # a request body would follow with no "&"
    return hmac.new(secret.encode(), total.encode(), hashlib.sha256).hexdigest()

doc = dict(symbol="BTCUSDT", side="BUY", type="LIMIT", quantity=1, price=11,
           recvWindow=5000, timestamp=1644489390087)      # MEXC's published example
assert sign("45d0b3c26f2644f19bfb98b07741b2f5", doc).startswith("fd3e4e85")
```

The assertion passes against MEXC's published example; a float raises `TypeError`, since `urlencode` would send 0.00001 as 1e-05. A timestamp more than one second ahead of the server is rejected, so take an offset from `/api/v3/time`. A signed `GET /api/v3/account` with a placeholder key returned HTTP 400, code 10072, Api key info invalid. Orders and private streams need a real key and were not run; see [exchange API key steps](https://fincodelabs.com/en/blog/exchange-api-key-how-to) and the [HMAC signature generator](https://fincodelabs.com/en/tools/hmac-signature-generator).

## Is there a MEXC demo trading API or testnet?

No test environment was found in MEXC's official API documentation read on 5 November 2026, so only public market data can be tested without an account.

The pages read give only `https://api.mexc.com` for REST; the official Python demo's README mentions a test environment URL but names none. `POST /api/v3/order/test` skips the matching engine but needs a real signed key. Futures, documented separately, are not covered; derivatives carry a high risk of loss. Test the rest against a mock exchange, as in [how to test trading bots](https://fincodelabs.com/en/blog/how-to-test-trading-bots). For integration work in general, see [exchange API integration](https://fincodelabs.com/en/services/exchange-api-integration); naming MEXC here does not mean Fincode integrates it or has built on it.

## Frequently asked questions

### Why does the MEXC WebSocket say Blocked?

The server refused the channel name: on 5 November 2026, old JSON channels, the non-aggregated deals channel, lowercase symbols, a 50ms interval and unknown symbols, all with code 0.

### Does the MEXC WebSocket still send JSON?

No: market data arrives as binary protobuf frames; only replies such as subscription acknowledgements and PONG are JSON text.

### How many streams can one MEXC WebSocket connection carry?

MEXC documents 30, and in testing the 31st was refused.

### Does MEXC have a testnet?

None was found in the official documentation read on 5 November 2026; the test order endpoint skips matching but needs a real key.

## Sources

- [MEXC API documentation: Spot V3 Introduction (limits, signing, FAQ), read 5 November 2026](https://www.mexc.com/api-docs/spot-v3/introduction)
- [MEXC API documentation: Websocket Market Streams](https://www.mexc.com/api-docs/spot-v3/websocket-market-streams/)
- [MEXC API documentation: Protocol Buffers Integration](https://www.mexc.com/api-docs/spot-v3/websocket-market-streams/protocol-buffers-integration)
- [MEXC API documentation: How to Properly Maintain a Local Copy of the Order Book](https://www.mexc.com/api-docs/spot-v3/websocket-market-streams/how-to-properly-maintain-a-local-copy-of-the-order-book)
- [MEXC API documentation: Order Book endpoint (limit up to 5000)](https://www.mexc.com/api-docs/spot-v3/market-data-endpoints/order-book)
- [MEXC API documentation: Error Code](https://www.mexc.com/api-docs/spot-v3/error-code)
- [MEXC API documentation: Change Log (latest entry 23 October 2026)](https://www.mexc.com/api-docs/spot-v3/change-log)
- [MEXC API documentation: Test New Order](https://www.mexc.com/api-docs/spot-v3/spot-account-trade/test-new-order)
- [GitHub: mexcdevelop/websocket-proto, protobuf definitions (Apache 2.0)](https://github.com/mexcdevelop/websocket-proto)
- [GitHub: mexcdevelop/mexc-api-demo, Python WebSocket example](https://github.com/mexcdevelop/mexc-api-demo/tree/main/python/websocket)
- [MEXC Spot V3 API documentation, older GitHub Pages copy (change log to 24 February 2025)](https://mexcdevelop.github.io/apidocs/spot_v3_en/)
- [Capital Markets Law No. 6362, Article 99/A (Turkish, consolidated text)](https://www.mevzuat.gov.tr/mevzuatmetin/1.5.6362.pdf)
- [SPK: Crypto-asset service providers in operation, temporary list (Turkish)](https://spk.gov.tr/kurumlar/kripto-varlik-hizmet-saglayicilar/faaliyette-bulunanlar-listesi)

> This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the [editorial policy](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/mexc-websocket-python

Cite as: Fincode, "MEXC WebSocket API in Python: protobuf and Blocked errors", updated November 6, 2026, https://fincodelabs.com/en/blog/mexc-websocket-python
