# Gate.io API in Python: v4 signing, TestNet and WebSocket

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

Gate.io API v4 in Python, docs read 5 Nov 2026: HMAC-SHA512 signing, the 60-second clock window, TestNet hosts, rate limits, WebSocket, tested code.

**Short answer:** The **Gate.io API in Python** (Gate API v4) authenticates private calls with three headers, KEY, Timestamp in seconds and SIGN, an HMAC-SHA512 hex digest of method, path, query, body hash and timestamp. Live REST calls go to `https://api.gateio.ws/api/v4` and TestNet calls, with separate keys, to `https://api-testnet.gateapi.io/api/v4` (official documentation read 5 November 2026).

Gate is a crypto exchange; its API v4 is unrelated to the Kubernetes Gateway API. Its documentation is on gate.com; live API hosts use gateio.ws. This guide follows documentation v4.106.179 (changelog to 3 November 2026); for general patterns see [crypto exchange API integration](https://fincodelabs.com/en/blog/crypto-exchange-api-integration).

**What ran.** On 5 November 2026, 22:39 to 23:12 UTC, Python 3.11.3 (requests 2.31.0, websockets 13.1) called Gate's public REST and WebSocket endpoints, live and TestNet; private calls used placeholder values only.

This guide recommends no platform or strategy. Automated trading can lose money, and derivatives such as futures carry a high risk of rapid loss. Availability depends on your jurisdiction and Gate's terms. For readers in Turkey: Article 99/A of Capital Markets Law No. 6362 treats activity by foreign crypto-asset platforms directed at Turkish residents as unauthorized.

## Gate API v4 base URLs: live, TestNet and WebSocket

REST and streams use different hosts, and TestNet has its own.

**Hosts in Gate's documentation, read 5 November 2026**

| Purpose | Live | TestNet |
| --- | --- | --- |
| REST | `https://api.gateio.ws/api/v4` | `https://api-testnet.gateapi.io/api/v4` |
| Spot WebSocket | `wss://api.gateio.ws/ws/v4/` | `wss://ws-testnet.gate.com/v4/ws/spot` |
| USDT futures WebSocket | `wss://fx-ws.gateio.ws/v4/ws/usdt` | `wss://ws-testnet.gate.com/v4/ws/futures/usdt` |

The 2019 changelog's TestNet host, `fx-api-testnet.gateio.ws`, returned HTTP 502 with an HTML page on 5 November 2026. The official SDK is `gate-api` on PyPI (7.2.179, 3 November 2026); the samples use plain `requests`.

## Gate API key: permissions, IP whitelist and TestNet keys

Create keys under APIv4 Keys with only the permissions the program needs, and withdrawal disabled for a bot.

- **Groups:** spot/margin, perpetual, delivery, wallet and withdrawal, each disabled, read-only or read-write.
- **IP whitelist:** up to 20 IPv4 addresses per key, no ranges; key changes take up to 5 minutes to apply.
- **Conflict:** the section says an account can create 20 keys, and also at most 5 with separate permissions; plan for 5.
- **TestNet keys** are separate, created in a Futures TestNet APIKeys tab; live keys do not work there.

Storage and rotation: [API key security](https://fincodelabs.com/en/blog/api-key-security-trading-bots).

## Gate.io API Python example: market data and order sizing

Market endpoints need no key; this reads, on both hosts, the limits an order must meet.

```python
from decimal import Decimal, ROUND_DOWN, ROUND_UP
import requests

HOSTS = {"live": "https://api.gateio.ws/api/v4", "testnet": "https://api-testnet.gateapi.io/api/v4"}

def public(base, path, **params):
    r = requests.get(base + path, params=params, timeout=10)
    try:
        body = r.json()
    except ValueError:                                     # an HTML 502 page is not JSON
        body = None
    if r.status_code == 200 and body is not None:
        return body, r.headers
    label = body.get("label") if isinstance(body, dict) else "no JSON body"
    raise RuntimeError(f"HTTP {r.status_code} {label}")

def only(rows, pair):
    hits = [row for row in rows if row.get("currency_pair") == pair]
    if len(hits) == 1:
        return hits[0]
    raise RuntimeError(f"{len(hits)} rows for {pair}")      # never pick one of several

def to_step(value, places, rounding=ROUND_DOWN):
    d = Decimal(str(value))
    ok = d.is_finite() and d.adjusted() < 13               # nan, inf and 1e400 fail
    q = d.quantize(Decimal(1).scaleb(-int(places)), rounding) if ok else Decimal(0)
    if q <= 0:                                             # 1e-400 rounds to zero
        raise ValueError(f"unusable number {value}")
    return q

spec, last = {}, {}
for name, base in HOSTS.items():
    spec[name], _ = public(base, "/spot/currency_pairs/BTC_USDT")
    rows, h = public(base, "/spot/tickers", currency_pair="BTC_USDT")
    last[name] = only(rows, "BTC_USDT")["last"]
    print(name, last[name], spec[name]["min_quote_amount"],
          h.get("X-Gate-RateLimit-Limit"), h.get("X-Gate-RateLimit-Requests-Remain"))

s = spec["live"]
price = to_step(Decimal(last["live"]) * Decimal("0.95"), s["precision"])          # hypothetical
need = Decimal(s["min_quote_amount"]) / price
for rounding in (ROUND_DOWN, ROUND_UP):
    amount = to_step(need, s["amount_precision"], rounding)
    print(price, amount, round(price * amount, 4), price * amount >= Decimal(s["min_quote_amount"]))

for test in (lambda: to_step("1e400", 1), lambda: to_step("1e-400", 1), lambda: to_step(float("1e400"), 1),
             lambda: only([{"currency_pair": "X"}] * 2, "X"), lambda: public(HOSTS["live"], "/spot/currency_pairs/NO_PAIR")):
    try:
        test()
    except (ValueError, RuntimeError) as err:
        print(err)
```

Output on 5 November 2026 at 23:01:31 UTC:

```
live 75596.4 3 200 199
testnet 78900 3 200 199
71816.5 0.000041 2.9445 False
71816.5 0.000042 3.0163 True
unusable number 1e400
unusable number 1e-400
unusable number inf
2 rows for X
HTTP 400 INVALID_CURRENCY
```

- **Status first, then the label.** Gate says to branch on `label`, not `message`; its example, scoped to business errors, reads `r.json()['label']` directly, which fails on an HTML 502 page. Status codes vary: a missing header returned 400, a stale timestamp 403.
- `min_quote_amount` may be null, meaning no limit; this sample does not handle that.

> **Hypothetical example.** 5% under 75,596.4 floors to 71,816.5, where the 3 USDT minimum needs 0.0000418 BTC. Floored to six decimals, 0.000041 BTC is 2.9445 USDT, under the minimum; rounded up, 0.000042 BTC is 3.0163 USDT.

## How to sign Gate API v4 requests in Python

Join method, path with `/api/v4`, unencoded query, hex SHA-512 of the body and timestamp with newlines; `SIGN` is the hex HMAC-SHA512 of that string.

```python
import hashlib, hmac, json, os, re, time
from decimal import Decimal
from urllib.parse import urlsplit
import requests

HOSTS = {"live": "https://api.gateio.ws/api/v4", "testnet": "https://api-testnet.gateapi.io/api/v4"}

def sign(secret, method, path, query, body, ts):
    text = "\n".join([method.upper(), path, query, hashlib.sha512(body.encode()).hexdigest(), ts])
    return hmac.new(secret.encode(), text.encode(), hashlib.sha512).hexdigest()

# Gate's two published examples
assert sign("secret", "GET", "/api/v4/futures/orders", "contract=BTC_USD&status=finished&limit=50",
            "", "1541993715").startswith("55f84ea195d6fe57")
assert sign("secret", "POST", "/api/v4/futures/orders", "", '{"contract":"BTC_USD","type":"limit",'
            '"size":100,"price":6800,"time_in_force":"gtc"}', "1541993715").startswith("eae42da914a590dd")

def private(method, base, path, params=None, body=None, skew=0):
    key, secret = os.environ.get("GATE_KEY", ""), os.environ.get("GATE_SECRET", "")
    if not (key and secret):                                # before any network call
        raise RuntimeError("set GATE_KEY and GATE_SECRET")
    query = "&".join(f"{k}={v}" for k, v in (params or {}).items())
    raw = json.dumps(body, separators=(",", ":")) if body else ""
    ts = str(int(time.time()) + skew)                       # seconds, not milliseconds
    sig = sign(secret, method, urlsplit(base).path + path, query, raw, ts)   # path keeps /api/v4
    r = requests.request(method, base + path + ("?" + query if query else ""), data=raw or None, timeout=10,
                         headers={"KEY": key, "Timestamp": ts, "SIGN": sig, "Content-Type": "application/json"})
    try:
        reply = r.json()
    except ValueError:
        reply = None
    if 200 <= r.status_code < 300:
        return reply
    label = reply.get("label") if isinstance(reply, dict) else "no JSON body"
    raise RuntimeError(f"HTTP {r.status_code} {label}")

TEXT, used = re.compile(r"t-[0-9A-Za-z_.-]{1,28}"), set()

def limit_order(base, pair, side, amount, price, text):
    for x in (amount, price):                               # inf, nan, 0, 1E+400 fail
        if not (isinstance(x, Decimal) and x.is_finite() and x > 0 and -13 < x.adjusted() < 13):
            raise ValueError(f"bad number {repr(x)}")
    if not TEXT.fullmatch(text or ""):                      # before the duplicate test
        raise ValueError(f"invalid text {repr(text)}")
    if text in used:
        raise ValueError(f"text {repr(text)} already used")
    used.add(text)
    return private("POST", base, "/spot/orders", body={
        "text": text, "currency_pair": pair, "type": "limit", "account": "spot", "side": side,
        "amount": format(amount, "f"), "price": format(price, "f"), "time_in_force": "poc"})

ok = Decimal("0.0001")
for amount, text in ((float("1e400"), "t-a"), (ok, ""), (ok, "run-0001"),
                     (ok, "t-run-0001"), (ok, "t-run-0001")):
    try:
        limit_order(HOSTS["testnet"], "BTC_USDT", "buy", amount, Decimal("20000"), text)
    except (ValueError, RuntimeError) as err:
        print(err)
os.environ.update(GATE_KEY="placeholder", GATE_SECRET="placeholder")
for name, skew in (("live", 0), ("live", -120), ("testnet", -400)):
    try:
        private("GET", HOSTS[name], "/spot/accounts", skew=skew)
    except RuntimeError as err:
        print(name, skew, err)
```

Output on 5 November 2026 at 22:59:26 UTC; the last three lines are Gate's replies to placeholder values:

```
bad number inf
invalid text ''
invalid text 'run-0001'
set GATE_KEY and GATE_SECRET
text 't-run-0001' already used
live 0 HTTP 401 INVALID_KEY
live -120 HTTP 403 REQUEST_EXPIRED
testnet -400 HTTP 401 INVALID_KEY
```

- **Checked.** Full signatures match both published examples; a fake-transport run showed signed and sent bytes match.
- **Seconds, within 60.** Measured against `GET /spot/time`, live accepted gaps of 59 seconds either way and rejected 61 with 403 `REQUEST_EXPIRED`, checking the clock before the key. This PC ran 1.7 seconds slow; keep NTP sync on. Milliseconds failed; the fractional `time.time()` in Gate's example passed.
- **Custom IDs.** `text` is `t-` plus up to 28 bytes of letters, digits, `_`, `-` or `.`. Single-order calls accept it only while the order is pending; another Spot passage allows 1 hour after it ends, so store Gate's order ID. Whether a reused `text` is rejected is not stated, so persist used IDs; see [client order IDs](https://fincodelabs.com/en/blog/client-order-id).
- `limit_order` never reached Gate: that needs a real TestNet key. See also API signature generation.

## Gate testnet: what it accepts that live does not

TestNet is a separate, thin market with a looser clock check, so passing there proves less.

**Live and TestNet on 5 November 2026**

| Item | Live | TestNet |
| --- | --- | --- |
| BTC_USDT last price, 23:11 UTC | 75,387.6 | 78,900 |
| BTC_USDT 24-hour volume, 23:11 UTC | 18,748.49 BTC | 0.32 BTC |
| Timestamp window | 60 seconds | 600 seconds per the error message; 590 off passed, 610 failed |

A clock five minutes off passes TestNet and fails live. Spot orders there were not tested. See [how to test trading bots](https://fincodelabs.com/en/blog/how-to-test-trading-bots).

## Gate API rate limit: per endpoint, per UID and fill ratio

Public endpoints allow 200 requests per 10 seconds per endpoint per IP; private limits count per UID.

**Gate frequency limit rule, read 5 November 2026**

| Endpoints | Limit |
| --- | --- |
| Public: order book, candlesticks, tickers | 200 per 10 seconds per endpoint, per IP |
| Spot placement and amend | 10 per second, per UID and market |
| Perpetual futures placement and amend | 100 per second |
| WebSocket connections | 300 per IP |

Responses carry `X-Gate-RateLimit-Limit`, `X-Gate-RateLimit-Requests-Remain` and `X-Gate-RateLimit-Reset-Timestamp` (200 and 199 above). The fill-ratio section is unclear on spot: it sets 10 requests per 10 seconds per UID for placement and amendment when USDT traded per placement, cancel or amend request over 24 hours is low (Gate suggests above 0.1), yet says spot limits will not be launched this time; WebSocket error 212 is spot fill-rate limiting. Design as if the spot rule applies; see [API rate limit exceeded](https://fincodelabs.com/en/blog/api-rate-limit-exceeded-crypto-exchanges) and [token bucket rate limiter](https://fincodelabs.com/en/blog/token-bucket-rate-limiter-python).

## Gate.io WebSocket API in Python: subscribe, ping and errors

Send JSON with `time`, `channel`, `event` and `payload`, and check each reply's `error` field, not `result`.

```python
import asyncio, json, time
import websockets

WS = {"live": "wss://api.gateio.ws/ws/v4/", "testnet": "wss://ws-testnet.gate.com/v4/ws/spot"}

async def first_ticker(url, pair):
    async with websockets.connect(url, open_timeout=10) as ws:   # library pings every 20 s
        await ws.send(json.dumps({"time": int(time.time()), "channel": "spot.ping"}))
        await ws.send(json.dumps({"time": int(time.time()), "id": 1, "channel": "spot.tickers",
                                  "event": "subscribe", "payload": [pair]}))
        while True:
            try:
                msg = json.loads(await asyncio.wait_for(ws.recv(), 10))
            except asyncio.TimeoutError:                          # a quiet market may push nothing
                return "no update within 10 s"
            if msg.get("error"):
                raise RuntimeError(f"{msg.get('channel')} {msg['error']}")
            if msg.get("event") == "update":
                return msg["result"]["last"]
            print(" ", msg.get("channel"), msg.get("result"))

for name, url in WS.items():
    print(name, asyncio.run(first_ticker(url, "BTC_USDT")))
```

Output on 5 November 2026 at 23:02:10 UTC:

```
  spot.pong None
  spot.tickers {'status': 'success'}
live 75645.1
  spot.pong None
  spot.tickers {'status': 'success'}
testnet 78900
```

- A failed subscription set `result` to `{"status": "fail"}` beside the error; the page says null.
- Public subscriptions with `time` 120 and 3,600 seconds stale were accepted, despite the documented 60-second limit.
- Private channels send an `auth` field with the key and an HMAC-SHA512 of `channel=<channel>&event=<event>&time=<time>`; a placeholder key got error 4, `INVALID_KEY`.
- No idle timeout is stated, though `spot.ping` resets a client timeout timer. With library pings off, an unsubscribed connection closed after 31 seconds in four runs; one sending `spot.ping` every 10 seconds, or subscribed to tickers, stayed open for the 100 and 330 seconds tested. See [WebSocket heartbeat and reconnect](https://fincodelabs.com/en/blog/websocket-heartbeat-reconnect).

For integration work in general, see [exchange API integration](https://fincodelabs.com/en/services/exchange-api-integration); naming Gate here does not mean Fincode integrates it or has built on it.

## Frequently asked questions

### Is the Gate API related to the Kubernetes Gateway API?

No: this is the Gate crypto exchange's API; the Kubernetes Gateway API is a Kubernetes routing project.

### Does Gate have a spot testnet?

Its TestNet hosts served public spot data on 5 November 2026, but TestNet keys are described only for futures, so spot trading there is unconfirmed.

### What timestamp does the Gate API expect?

Unix seconds, within 60 seconds of server time on live; milliseconds return REQUEST_EXPIRED, though GET /spot/time reports milliseconds.

### Is there an official Gate Python SDK?

Yes: gate-api on PyPI, version 7.2.179 as of 3 November 2026.

## Sources

- [Gate API v4 documentation: access URLs, frequency limit rule, fill-ratio rules, error labels, authentication (v4.106.179)](https://www.gate.com/docs/developers/apiv4/en/)
- [Gate API v4: Spot (currency pairs, tickers, create order, text field rules, server time)](https://www.gate.com/docs/developers/apiv4/en/spot/)
- [Gate Spot WebSocket v4 (server URLs, request format, authentication, ping, tickers channel)](https://www.gate.com/docs/developers/apiv4/ws/en/)
- [Gate Futures WebSocket v4 (server URLs)](https://www.gate.com/docs/developers/futures/ws/en/)
- [PyPI: gate-api, official Gate API v4 Python SDK](https://pypi.org/project/gate-api/)
- [GitHub: gate/gateapi-python README](https://github.com/gate/gateapi-python)
- [Gate live REST: BTC_USDT currency pair details](https://api.gateio.ws/api/v4/spot/currency_pairs/BTC_USDT)
- [Gate TestNet REST: BTC_USDT currency pair details](https://api-testnet.gateapi.io/api/v4/spot/currency_pairs/BTC_USDT)
- [Kubernetes Gateway API: Introduction](https://gateway-api.sigs.k8s.io/docs/introduction/)
- [websockets library documentation: Keepalive and latency (ping every 20 seconds by default)](https://websockets.readthedocs.io/en/stable/topics/keepalive.html)
- [Capital Markets Law No. 6362, Article 99/A (Turkish, consolidated text)](https://www.mevzuat.gov.tr/mevzuatmetin/1.5.6362.pdf)

> 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/gate-io-api-python

Cite as: Fincode, "Gate.io API in Python: v4 signing, TestNet and WebSocket", updated November 6, 2026, https://fincodelabs.com/en/blog/gate-io-api-python
