Short answer: To use the Bitget API in Python, sign timestamp + method + path + ?query + body with HMAC-SHA256, Base64-encode it, and send it with your key, millisecond timestamp and passphrase in four ACCESS headers to https://api.bitget.com. Bitget recommends its Unified Trading Account (v3 paths, streams at wss://ws.bitget.com/v3/ws/public); demo trading takes a demo key plus the header paptrading: 1 (official documentation read on 5 November 2026).
Bitget's introduction pages, read 5 November 2026, recommend the Unified Trading Account (UTA) and say the Classic account is in maintenance mode, so this guide uses UTA v3; background is in crypto exchange API integration.
What ran. On 5 November 2026, 19:09 to 20:11 UTC, Python 3.11.3 (requests 2.31.0, websockets 13.1) called Bitget's public production endpoints and its live and demo public streams. Signed calls used placeholder values only; no order was placed.
This guide recommends no platform or strategy. Automated trading can lose money, and derivatives carry a high risk of rapid loss. Availability depends on your jurisdiction and Bitget'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.
Bitget API Python setup: UTA v3, Classic v2 and the hosts
Use UTA v3 unless your account is still on Classic; paths, hosts and message fields differ.
| Purpose | UTA (v3) | Classic (v2) |
|---|---|---|
| REST | https://api.bitget.com, /api/v3/ paths | /api/v2/ paths |
| Public stream | wss://ws.bitget.com/v3/ws/public | .../v2/ws/public |
| Private stream | wss://ws.bitget.com/v3/ws/private | .../v2/ws/private |
| Demo | REST: header paptrading: 1; streams: host wspap.bitget.com | The same, v2 paths |
One page disagrees: /docs/uta/rest-api carries the Classic REST page's text, with v2 stream hosts and 10 keys per UID, while the UTA Quick Start lists v3 hosts and 50 key sets. Follow the Quick Start.
Bitget API key and passphrase: headers and auth errors
Private calls carry ACCESS-KEY, ACCESS-SIGN, ACCESS-TIMESTAMP and ACCESS-PASSPHRASE; the Quick Start says the passphrase you chose cannot be retrieved or changed, so losing it means a new key. See the exchange API key guide for creating keys and API key security for storing them.
| Code | Meaning | Note |
|---|---|---|
| 40006 | Invalid ACCESS_KEY | Seen with a missing or empty key |
| 40037 | Apikey does not exist | Seen with a placeholder key; not on Bitget's error pages |
| 40008 | Request timestamp expired | Sync with server time |
| 40009 | sign signature error | Error page: do not URL-encode before signing; the Quick Start's RSA sample does |
The key is checked first: timestamps 25 and 60 seconds off still returned 40037, so the documented 30-second window went untested.
Bitget API Python example: market data and order sizing
Market endpoints need no key, so this runs as is and reads the rules an order must pass.
from decimal import Decimal, ROUND_DOWN
import requests
BASE = "https://api.bitget.com"
def public(path, **params):
r = requests.get(BASE + path, params=params, timeout=10)
try:
body = r.json()
except ValueError:
body = {}
if r.status_code == 200 and body.get("code") == "00000":
return body.get("data")
raise RuntimeError(f"HTTP {r.status_code} {body.get('code')}: {body.get('msg')}")
def only(rows, symbol):
hits = [row for row in rows or [] if row.get("symbol") == symbol]
if len(hits) == 1:
return hits[0]
raise RuntimeError(f"{len(hits)} rows for {symbol}") # never pick one of several
def floor_to(value, places):
d = Decimal(str(value))
ok = d.is_finite() and d.adjusted() < 13 # nan, inf, 1e400 fail
q = d.quantize(Decimal(1).scaleb(-int(places)), ROUND_DOWN) if ok else Decimal(0)
if q <= 0: # 1e-400 floors to 0.00
raise ValueError(f"unusable number {value}")
return q
spec = only(public("/api/v3/market/instruments", category="SPOT", symbol="BTCUSDT"), "BTCUSDT")
last = only(public("/api/v3/market/tickers", category="SPOT", symbol="BTCUSDT"), "BTCUSDT")["lastPrice"]
print(spec["status"], last, spec["pricePrecision"], spec["quantityPrecision"], spec["minOrderAmount"])
price = floor_to(Decimal(last) * Decimal("0.95"), spec["pricePrecision"]) # hypothetical
for qty in (Decimal("0.0000139"), floor_to("0.0000139", spec["quantityPrecision"])):
print(price, qty, round(price * qty, 4), price * qty >= Decimal(spec["minOrderAmount"]))
for test in (lambda: floor_to("1e400", 2), lambda: floor_to("1e-400", 2),
lambda: floor_to(float("1e400"), 2), lambda: only([{"symbol": "X"}] * 2, "X"),
lambda: public("/api/v3/market/instruments", category="SPOT", symbol="NOSUCHPAIR")):
try:
test()
except (ValueError, RuntimeError) as err:
print(err)
Output on 5 November 2026 at 19:33 UTC:
online 76182.48 2 6 1
72373.35 0.0000139 1.0060 True
72373.35 0.000013 0.9409 False
unusable number 1e400
unusable number 1e-400
unusable number inf
2 rows for X
HTTP 400 40034: Parameter NOSUCHPAIR does not exist
- Errors arrive in the body, here HTTP 400, code
40034anddatanull: check status andcodefirst. - Round, then check the minimum. Bitget's error page says the backend trims quantity to the pair's precision, which can push an order under the minimum value (1 USDT spot, 5 USDT futures): error 45110.
- The order book returns JSON numbers, not strings; parse it with
r.json(parse_float=Decimal).
Hypothetical example. Above, 5% under the last price of 76,182.48 floors to 72,373.35. A wanted 0.0000139 BTC (1.0060 USDT) passes a 1 USDT check before rounding; floored to 6 decimals it is 0.000013 (0.9409 USDT), under the minimum.
How to sign Bitget API requests in Python
Sign timestamp + uppercase method + path + ? + query (if any) + body with HMAC-SHA256, Base64-encode the result, and send exactly the query and body you signed. RSA keys sign the same string with SHA-256 instead.
import base64, hashlib, hmac, json, os, re, time
from decimal import Decimal
import requests
BASE = "https://api.bitget.com"
def prehash(ts, method, path, query="", body=""):
return ts + method.upper() + path + ("?" + query if query else "") + body
def sign(secret, text):
return base64.b64encode(hmac.new(secret.encode(), text.encode(), hashlib.sha256).digest()).decode()
# Quick Start example, RFC 4231 test case 2
assert prehash("16273667805456", "GET", "/api/v3/account/fee-rate", "category=SPOT&symbol=BTCUSDT") \
== "16273667805456GET/api/v3/account/fee-rate?category=SPOT&symbol=BTCUSDT"
assert sign("Jefe", "what do ya want for nothing?") == "W9zBRr9gdU5qBCQmCJV1x1oAPwidJzmDnexYuWTsOEM="
def private(method, path, params=None, body=None, demo=True):
key, secret, phrase = (os.environ.get(n, "") for n in ("BITGET_KEY", "BITGET_SECRET", "BITGET_PASSPHRASE"))
if not (key and secret and phrase): # before any network call
raise RuntimeError("set BITGET_KEY, BITGET_SECRET and BITGET_PASSPHRASE")
query = "&".join(f"{k}={v}" for k, v in sorted((params or {}).items())) # sorted, as Bitget's examples do
raw = json.dumps(body, separators=(",", ":")) if body else ""
ts = str(int(time.time() * 1000))
headers = {"ACCESS-KEY": key, "ACCESS-SIGN": sign(secret, prehash(ts, method, path, query, raw)),
"ACCESS-TIMESTAMP": ts, "ACCESS-PASSPHRASE": phrase, "Content-Type": "application/json"}
if demo:
headers["paptrading"] = "1"
r = requests.request(method, BASE + path + ("?" + query if query else ""),
data=raw or None, headers=headers, timeout=10)
try:
reply = r.json()
except ValueError:
reply = {}
if r.status_code == 200 and reply.get("code") == "00000":
return reply.get("data")
raise RuntimeError(f"HTTP {r.status_code} {reply.get('code')}: {reply.get('msg')}")
OID, used = re.compile(r"[.A-Z:/a-z0-9_-]{1,32}"), set() # Place Order page pattern
def limit_order(symbol, side, qty, price, oid): # Decimals from floor_to
if not all(isinstance(x, Decimal) and x.is_finite() and x > 0 for x in (qty, price)):
raise ValueError(f"bad qty or price: {repr(qty)}, {repr(price)}")
if not OID.fullmatch(oid or ""): # before the duplicate test
raise ValueError(f"invalid clientOid {repr(oid)}")
if oid in used:
raise ValueError(f"clientOid {repr(oid)} already used")
used.add(oid)
return private("POST", "/api/v3/trade/place-order", body={
"category": "SPOT", "symbol": symbol, "side": side, "orderType": "limit", "timeInForce": "post_only",
"qty": format(qty, "f"), "price": format(price, "f"), "clientOid": oid})
for oid in ("", "order#1", "run-0001", "run-0001"):
try:
limit_order("BTCUSDT", "buy", Decimal("0.0001"), Decimal("20000"), oid)
except (ValueError, RuntimeError) as err:
print(err)
os.environ.update(BITGET_KEY="placeholder", BITGET_SECRET="placeholder", BITGET_PASSPHRASE="placeholder")
try:
private("GET", "/api/v3/account/assets")
except RuntimeError as err:
print(err)
Output on 5 November 2026 at 20:11 UTC; the last line is Bitget's reply to placeholder values:
invalid clientOid ''
invalid clientOid 'order#1'
set BITGET_KEY, BITGET_SECRET and BITGET_PASSPHRASE
clientOid 'run-0001' already used
HTTP 400 40037: Apikey does not exist
- A fake-transport run confirmed that signed and sent bytes match and that
Decimal("1E-8")goes out as0.00000001; without the Decimal check,float("1e400")went out asinf. Other causes of rejected signatures: API signature generation. - Bitget rejects a reused clientOid (25212) only while the first order is open. After a 40010, 40725 or 45001 reply, the Place Order page says to query the order by clientOid before resending. Keep used IDs in storage that survives restarts; this sample holds them in memory.
limit_ordernever reached Bitget: that needs a real demo key.
How to do demo trading on Bitget with the API
Switch the account to Demo mode, create a Demo API key there, and send it with the header paptrading: 1; streams use wspap.bitget.com.
Bitget's Classic demo pages say KYC is needed; the UTA Quick Start is silent. Two things the documents omit, seen on 5 November 2026 around 19:11 UTC:
- The demo market has its own prices. With the header, spot BTCUSDT last traded at 75,862.31 while the live ticker, under a second apart, showed 75,965.17; demo 24-hour turnover was about 19.5 million USDT, live about 333 million.
- Some endpoints are not routed. With the header, both server-time endpoints and
/api/v3/account/fee-ratereturned HTTP 404 (code 40404). Read server time from/api/v3/market/timewithout it.
Demo fills say little about live liquidity; demo tests software, not a strategy; see paper trading APIs.
Bitget API rate limit: per IP, per endpoint and per UID
Bitget allows each IP 6,000 requests a minute across REST and WebSocket, sets a limit on every endpoint, and answers HTTP 429 beyond them.
| Limit | Value |
|---|---|
| Overall | 6,000 requests per IP per minute |
| Market data | 20 per second per IP; RPI endpoints 10 |
| Place order | 10 per second per UID |
| Stream connections | 300 per IP per 5 minutes; 100 open per IP |
| Subscriptions | 240 per hour per connection; under 50 channels advised |
| Stream messages | 10 per second per connection, pings included |
Responses carry x-mbx-used-remain-limit, the quota left for that endpoint: quick ticker calls on 5 November 2026 read between 17 and 19, not always falling.
Open-order caps conflict: for spot, the Place Order page says 400 across spot and margin, while the error page entry for 45118 says 1,000. Design for the lower figure.
Bitget WebSocket URL: subscribe, ping and login
Connect to wss://ws.bitget.com/v3/ws/public, subscribe, and send the text ping every 30 seconds even while data flows; the Quick Start says the server drops a connection after 2 minutes without one.
v3 subscriptions name instType, topic and symbol; v2 uses channel and instId. Tests on 5 November 2026:
- The live and demo v3 hosts streamed ticker snapshots; a v3 message on the v2 host, or the reverse, returned error 30001.
- Without the text ping, streams dropped after 128 to about 150 seconds, both with no pings and with the websockets library's default protocol ping frames every 20 seconds; with the text ping every 30 seconds they were still open at 170 and 201 seconds (two runs each).
- Private streams log in with a signature over timestamp +
GET+/user/verify. The Quick Start's text and JavaScript line use milliseconds, its Java steps, login example and the official SDK seconds; a placeholder key returned 30011 either way, so the unit is unverified.
The official Bitget Python SDK: what its code does
Bitget's documentation points Python users to the bitget-python-sdk-api folder of its v3-bitget-api-sdk repository: source to copy, not a pip package. Its code was read on 5 November 2026, not run.
| Topic | What the code shows |
|---|---|
| Install | Docs list requests and websocket-client; the code also imports pycryptodome |
| Versions | Docs say v2; a v3 package arrived when v1 was removed; the README still shows v1 |
| Logging | Prints REST response bodies, and the stream login message with the passphrase |
| Gaps | No REST timeouts or paptrading header; use_server_time=True reads an undefined constant |
Pin a commit and fix those first, or see the CCXT Python guide. For integration work in general, see exchange API integration; naming Bitget here does not mean Fincode integrates it or has built on it.
Frequently asked questions
What is the Bitget WebSocket URL?
For the Unified Trading Account, wss://ws.bitget.com/v3/ws/public and /v3/ws/private; Classic uses v2 paths and demo trading the wspap.bitget.com host (read 5 November 2026).
How do I do demo trading on Bitget with the API?
Switch to Demo mode, create a Demo API key there, and add the header paptrading: 1 to REST calls.
What happens if I lose my Bitget API passphrase?
It cannot be retrieved or changed, so you delete the key and create a new one.
Is there an official Bitget Python SDK on pip?
Bitget's documentation names none: you copy the bitget-python-sdk-api folder from its v3-bitget-api-sdk repository.
Sources
- Bitget API: Introduction (recommends the Unified Trading Account; Classic in maintenance mode) (opens in a new tab)
- Bitget API: UTA Quick Start (keys, demo trading, domains, headers, signature, rate limit, WebSocket) (opens in a new tab)
- Bitget API: Classic account Introduction (opens in a new tab)
- Bitget API: REST API page under the UTA path, same text as the Classic REST API page (opens in a new tab)
- Bitget API: Classic Demo Trading, REST API (opens in a new tab)
- Bitget API: Classic Demo Trading, WebSocket (opens in a new tab)
- Bitget API: Get Instruments (opens in a new tab)
- Bitget API: Get Order Book (opens in a new tab)
- Bitget API: Order management, Place Order (clientOid pattern, order limits) (opens in a new tab)
- Bitget API: Common Error Handling (opens in a new tab)
- Bitget API: WebSocket Tickers Channel (opens in a new tab)
- Bitget API: Python SDK (opens in a new tab)
- GitHub: BitgetLimited v3-bitget-api-sdk, bitget-python-sdk-api folder (opens in a new tab)
- RFC 4231: Identifiers and Test Vectors for HMAC-SHA-224, HMAC-SHA-256, HMAC-SHA-384 and HMAC-SHA-512 (opens in a new tab)
- Capital Markets Law No. 6362, Article 99/A (Turkish, consolidated text) (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.