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"}.
| 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.
| 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.
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
gentosys.path. Code generated as protobuf 7.35.1 failed withVersionErrorunder 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
tradeIdfield, disables TLS certificate checks (sslopt={"cert_reqs": 0}) and ignores refusals. Regenerate rather than copy. - Fields. On aggregated deals,
createTimecame back 0 andsymbolIdempty, whiletradeIdheld strings such as747104443186495488X1_747104442330845184X3. Prices are strings: parse them withDecimal, notfloat.
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.
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.
MEXC WebSocket order book: versions and the snapshot
MEXC's book procedure is the usual snapshot plus diffs, keyed on fromVersion and toVersion.
- Subscribe to the aggregated depth channel and buffer diffs.
- Fetch
GET /api/v3/depth?symbol=SYMBOL&limit=5000; keeplastUpdateId. - Drop diffs whose
toVersionis not above it; if the first remaining diff starts abovelastUpdateId + 1, fetch again. - Apply in order; each
fromVersionmust equal the previoustoVersion+ 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.
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.
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 and the 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. For integration work in general, see 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 (opens in a new tab)
- MEXC API documentation: Websocket Market Streams (opens in a new tab)
- MEXC API documentation: Protocol Buffers Integration (opens in a new tab)
- MEXC API documentation: How to Properly Maintain a Local Copy of the Order Book (opens in a new tab)
- MEXC API documentation: Order Book endpoint (limit up to 5000) (opens in a new tab)
- MEXC API documentation: Error Code (opens in a new tab)
- MEXC API documentation: Change Log (latest entry 23 October 2026) (opens in a new tab)
- MEXC API documentation: Test New Order (opens in a new tab)
- GitHub: mexcdevelop/websocket-proto, protobuf definitions (Apache 2.0) (opens in a new tab)
- GitHub: mexcdevelop/mexc-api-demo, Python WebSocket example (opens in a new tab)
- MEXC Spot V3 API documentation, older GitHub Pages copy (change log to 24 February 2025) (opens in a new tab)
- Capital Markets Law No. 6362, Article 99/A (Turkish, consolidated text) (opens in a new tab)
- SPK: Crypto-asset service providers in operation, temporary list (Turkish) (opens in a new tab)
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.