Short answer: The Deribit API in Python needs no SDK: it is JSON-RPC 2.0 over WebSocket or HTTP, so requests and websockets are enough. Public methods need no key; private ones need a token from public/auth, and test.deribit.com needs its own account and key. Each sub-account has a credit pool (by default 500 credits per non-matching request, 50,000 at most, 10,000 refilled a second), and an empty pool returns error 10028 and ends the session.
Deribit's quickstart, read on 5 November 2026, gives curl and JavaScript, and its docs home names no SDK. The code below ran that day on Python 3.11.3 (requests 2.31.0, websockets 13.1) against the test host's public methods, and failure paths against fake replies and a local mock server. No real key was used, so tokens, private methods and orders are untested. General patterns: crypto exchange API integration.
This describes documentation and recommends no platform, product or strategy. Futures, perpetuals and options are leveraged and high risk; Deribit's terms warn you may lose all the crypto-assets in your account (see trading bot risk management). Its Restricted Jurisdictions page, updated 6 October 2026, bars anyone located or resident in countries including the United States, Canada and Japan, and UK retail clients. Article 99/A of Capital Markets Law No. 6362 treats activity by foreign crypto-asset platforms directed at Turkish residents as unauthorized.
Deribit API basics: hosts, testnet and connection limits
Every call is a JSON-RPC 2.0 message naming a method such as public/ticker, sent over WebSocket (recommended) or HTTP.
| Environment | HTTP | WebSocket |
|---|---|---|
| Test | https://test.deribit.com/api/v2 | wss://test.deribit.com/ws/api/v2 |
| Production | https://www.deribit.com/api/v2 | wss://www.deribit.com/ws/api/v2 |
- Separate: test and production have separate accounts, keys and rate-limit pools; every reply carries a
testnetflag. - Limits: 32 open connections per IP, HTTP and WebSocket combined; 16 sessions per API key (the scope page says per user); 8 keys per account or subaccount; HTTP connections expire after 15 minutes.
- WebSocket only: subscriptions, heartbeats, cancel on disconnect. No batches: a JSON array returned 11050
bad_request.
On 5 November 2026 www.deribit.com did not complete a TLS handshake from the test machine, so nothing here touched production.
A tested Deribit API Python example over HTTP
One helper covers HTTP: POST the JSON-RPC body, treat anything without both a 200 status and a result as an error, and refuse replies not flagged testnet.
import time
import requests
BASE = "https://test.deribit.com/api/v2/" # test host: its own account and keys
class DeribitError(Exception):
pass
def rpc(method, **params):
body = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params}
r = requests.post(BASE + method, json=body, timeout=10)
try:
reply = r.json()
except ValueError: # an HTML error page, say
reply = None
if not isinstance(reply, dict):
reply = {}
if not (r.status_code == 200 and "result" in reply):
err = reply.get("error") # JSON-RPC errors come with HTTP 400
if not isinstance(err, dict):
err = {"message": err}
raise DeribitError(f"HTTP {r.status_code}: {err.get('code')} "
f"{err.get('message')} {err.get('data', '')}")
if reply.get("testnet") is not True: # refuse to talk to production
raise DeribitError("reply is not from the test environment")
return reply["result"]
def instrument(name, currency="BTC"): # a costly call: cache it
rows = [i for i in rpc("public/get_instruments", currency=currency)
if i.get("instrument_name") == name]
if len(rows) == 1: # not missing, not ambiguous
return rows[0]
raise DeribitError(f"{len(rows)} instruments named {repr(name)}")
skew = rpc("public/get_time") - time.time() * 1000
print(f"clock skew {skew:+.0f} ms") # signatures expire after 60 s
perp = instrument("BTC-PERPETUAL") # only state "open" takes orders
print("instrument", perp["state"], "tick", perp["tick_size"],
"min amount", perp["min_trade_amount"])
t = rpc("public/ticker", instrument_name="BTC-PERPETUAL")
print("ticker", t["state"], "bid", t["best_bid_price"], "ask", t["best_ask_price"])
try:
rpc("public/ticker", instrument_name="BTC-NOPE")
except DeribitError as e:
print("error:", e)
Output at 21:03 UTC on 5 November 2026 (prices and states change):
clock skew +1086 ms
instrument locked tick 0.5 min amount 10.0
ticker open bid 78562.5 ask 78563.0
error: HTTP 400: -32602 Invalid params {'reason': 'wrong format', 'param': 'instrument_name'}
- Errors came back as HTTP 400 with a JSON body, so raising on the status first loses the code.
public/get_instrumentscosts 10,000 credits and is cached for about a minute. Only stateopentakes orders;lockedallows cancels only. In checks from 20:18 to 21:05 UTC, every BTC entry on the test host was locked or halted andpublic/statusreturnedlocked: true, yet the ticker saidopen.- Names seen:
BTC-PERPETUAL,BTC-6NOV26(future),BTC-6NOV26-70000-C(option). Prices must sit on the tick (options addtick_size_steps) or orders fail with 10043. Ticker funding fields are explained in perpetual futures funding rate.
How to get a Deribit API key and authenticate
Create the first key in the web interface of the host you will use (Account, API, Add new key); later keys can come from private/create_api_key.
- Pick a Deribit-generated key (client ID and a secret shown once) or a self-generated Ed25519 or RSA key pair.
- Scopes cap what a token gets:
account,trade,wallet,block_trade,block_rfq, each:reador:read_write.wallet:read_writeallows withdrawals, which a trading bot does not need. Add an IP whitelist and checkscopein the auth reply, since the server may grant less. See API key security for trading bots.
| grant_type | You send | Note |
|---|---|---|
client_credentials | Client ID and secret | Simplest; the secret travels |
client_signature | Client ID, timestamp, nonce, data, signature: HMAC-SHA256 hex, or Ed25519/RSA signature in unpadded base64url | Timestamp valid 60 seconds; nonce used once |
refresh_token | A refresh token | New access and refresh tokens |
Send the token as access_token in WebSocket params or as Authorization: Bearer over HTTP. Over WebSocket, the default connection token dies with its connection; a session:name token survives reconnects. The article's Python snippet sits under its WebSocket heading but uses the HTTP formula, which adds method, URI and body. The WebSocket version, in the same file, checked against the article's example:
import hashlib, hmac, secrets
def signature_auth(client_id, client_secret, data="", ts=None, nonce=None):
if not client_id or not client_secret: # fail before any network call
raise DeribitError("client_id and client_secret are required")
ts = ts or int(time.time() * 1000) # valid for 60 seconds
nonce = nonce or secrets.token_hex(8) # use once
data = data or ""
msg = f"{ts}\n{nonce}\n{data}".encode()
sig = hmac.new(client_secret.encode(), msg, hashlib.sha256).hexdigest()
return {"grant_type": "client_signature", "client_id": client_id,
"timestamp": ts, "nonce": nonce, "data": data, "signature": sig}
# the published example from Deribit's authentication article
print(signature_auth("AMANDA", "AMANDASECRECT", ts=1576074319000,
nonce="1iqt2wls")["signature"])
56590594f97921b09b18f166befe0d1319b198bbcdad7ca73382de2f88fe9aa1
Deribit API WebSocket in Python: heartbeats and the book
Keep one connection, turn on heartbeats, check each subscribe reply, answer every test_request with public/test, and rebuild the book from a snapshot when change IDs break. The same file continues:
import asyncio, json
import websockets
WS = "wss://test.deribit.com/ws/api/v2"
CHANNELS = ["ticker.BTC-PERPETUAL.100ms", "book.BTC-PERPETUAL.100ms",
"book.ETH-PERPETUAL.100ms"]
async def stream(seconds):
async with websockets.connect(WS) as ws:
print("connected")
next_id, wanted, last, resub_at = 0, {}, {}, {}
async def call(method, **params):
nonlocal next_id
next_id += 1
if method.endswith("/subscribe"):
wanted[next_id] = set(params["channels"])
await ws.send(json.dumps({"jsonrpc": "2.0", "id": next_id,
"method": method, "params": params}))
loop = asyncio.get_running_loop()
end = loop.time() + seconds
await call("public/set_heartbeat", interval=30)
await call("public/subscribe", channels=CHANNELS)
while loop.time() < end:
msg = json.loads(await asyncio.wait_for(ws.recv(), 65))
err = msg.get("error")
if err is not None: # 10028 ends the session: back off
code = err.get("code") if isinstance(err, dict) else None
raise (ConnectionError if code == 10028 else DeribitError)(err)
if msg.get("id") in wanted: # the reply lists what worked
missing = wanted.pop(msg["id"]) - set(msg.get("result") or [])
if missing:
raise DeribitError(f"not subscribed: {sorted(missing)}")
p = msg.get("params") or {}
if msg.get("method") == "heartbeat":
if p.get("type") == "test_request":
await call("public/test") # the only valid answer
print("answered test_request")
continue
ch, d = p.get("channel", ""), p.get("data") or {}
if not (msg.get("method") == "subscription" and ch.startswith("book.")):
continue
if d.get("type") == "snapshot": # one state per book channel
last[ch] = d["change_id"]
print(ch, "snapshot:", len(d["bids"]), "bids,", len(d["asks"]), "asks")
elif ch in last and d.get("prev_change_id") == last[ch]:
last[ch] = d["change_id"]
elif loop.time() >= resub_at.get(ch, 0): # a gap, or no snapshot yet
print(ch, "out of sync: resubscribing")
last.pop(ch, None)
resub_at[ch] = loop.time() + 5
await call("public/unsubscribe", channels=[ch])
await call("public/subscribe", channels=[ch])
async def main(rounds=2, seconds=40):
delay = 1
for _ in range(rounds): # a service loops forever
try:
await stream(seconds)
delay = 1
except (OSError, asyncio.TimeoutError, websockets.ConnectionClosed,
websockets.InvalidHandshake) as e:
print(f"connection lost ({repr(e)}); retry in {delay} s")
await asyncio.sleep(delay)
delay = min(delay * 2, 60)
asyncio.run(main())
Output at 21:04 UTC on 5 November 2026, with the test host locked:
connected
book.ETH-PERPETUAL.100ms snapshot: 73 bids, 8 asks
answered test_request
connected
book.ETH-PERPETUAL.100ms snapshot: 73 bids, 8 asks
answered test_request
- A snapshot may not come. The notifications page says the first book message is the whole book; the JSON-RPC overview says it typically is. At 20:24 UTC, with the test host locked, 8 of 8 fresh ETH book subscriptions began with a snapshot, but
book.BTC-PERPETUAL.100mssent nothing in three 8-second tries. - Silent failures: a misspelled channel returned an empty list, no error, hence
wanted. Therawinterval needs authentication (13778). - Mock-server tests (ten cases) passed, including a gap, a change before any snapshot, two interleaved books, 10028, close code 4000 and HTTP 429 at connect.
Background: WebSocket heartbeat and reconnect and local order book sync.
Deribit API rate limits: credits, tiers and error 10028
Each sub-account has a credit pool that refills continuously; a request that finds it empty gets 10028 too_many_requests and the session is terminated.
| Requests | Cost | Pool | Sustained | Burst |
|---|---|---|---|---|
| Non-matching (default) | 500 | 50,000 | 20 a second | 100 |
public/get_instruments | 10,000 | 500,000 | 1 a second | 50 |
public/subscribe, private/subscribe | 3,000 | 30,000 | About 3.3 a second | 10 |
| Tier | 7-day volume | Sustained | Burst |
|---|---|---|---|
| 1 | Over USD 25 million | 30 a second | 100 |
| 2 | Over USD 5 million | 20 a second | 50 |
| 3 | Over USD 1 million | 10 a second | 30 |
| 4 | Up to USD 1 million | 5 a second | 20 |
- Unauthenticated requests are limited per IP (no figures given) and risk IP bans more.
- Using the web platform spends credits too.
- Real limits come from
private/get_account_summary(needs a key; not run); the page's default-setup sample shows non-matchingburst1,500 andrate1,000, not 100 and 20 a second, with no time unit.
Hypothetical example. An authenticated script polling at 25 requests a second spends 25 x 500 = 12,500 credits a second against a 10,000 refill, a net 2,500, so a full pool lasts 50,000 / 2,500 = 20 seconds before 10028.
Deribit API error codes and the terms a bot must follow
On 5 November 2026 the test host's codes matched Deribit's complete error reference, not the common-codes table on the same page.
| Code | Message | Trigger |
|---|---|---|
| 13004 | invalid_credentials | public/auth with placeholders |
| 13009 | unauthorized | Private method without a token |
| -32602 | Invalid params | Unknown instrument or currency |
| 10030 | must_be_websocket_request | public/set_heartbeat over HTTP |
The common-codes table gives invalid_credentials as 10001 and 13009 to 13011 as token errors; the complete reference, like the server, uses 13010 and 13011 for missing or too-short values. Map codes, not messages, and log data.
Deribit's Terms of Service for DRB Panama Inc. (version 2 February 2026) bar market abuse, including spoofing, and, in clause 23, "systematic or automated data collection activities" on its systems; they do not say how that applies to API market data, so ask Deribit before building a collector. For integration work in general, see exchange API integration; naming Deribit here does not mean Fincode integrates it or has built on it.
Frequently asked questions
Does Deribit have an official Python SDK?
The documentation read on 5 November 2026 names none, and no URL in its sitemap suggests one. PyPI's deribit-api package lists Deribit as author, but its last release was in September 2017 and its GitHub link returned 404 that day.
How do I get a Deribit testnet API key?
Open an account on test.deribit.com, separate from www.deribit.com, and add a key under Account, API. Copy the client secret at once; it is shown only at creation.
Why does my Deribit WebSocket close with code 4000?
Heartbeats are on (minimum interval 10 seconds; the usage policy recommends 30 to 60) and your client sent nothing for twice the interval plus 500 ms, usually because a test_request went unanswered. Reply with public/test; ping frames do not count.
What does Deribit error 13009 mean?
Unauthorized: a missing, expired or wrong token, or a bad signature. A connection-scoped token dies with its connection, so authenticate again after reconnecting.
Sources
- Deribit API documentation: home (interfaces and environments) (opens in a new tab)
- Deribit API: Quickstart Guide (opens in a new tab)
- Deribit API: JSON-RPC 2.0 protocol overview (transports, testnet field, no batching) (opens in a new tab)
- Deribit API: Authentication with OAuth 2.0 and signatures (opens in a new tab)
- Deribit API: Creating a new API key (opens in a new tab)
- Deribit API: Access scope (opens in a new tab)
- Deribit API: Rate limits (credit system and matching-engine tiers) (opens in a new tab)
- Deribit API: Connection management (limits, heartbeats, cancel on disconnect) (opens in a new tab)
- Deribit API: Notifications and subscriptions (book snapshots and change IDs) (opens in a new tab)
- Deribit API reference: public/get_instruments (cost, caching, states) (opens in a new tab)
- Deribit API: API usage policy (heartbeat frequency, errors and IP bans) (opens in a new tab)
- Deribit API: Error codes (opens in a new tab)
- Deribit Support: Restricted Jurisdictions (updated 6 October 2026) (opens in a new tab)
- Deribit Support: Terms of Service, DRB Panama Inc. (version 2 February 2026) (opens in a new tab)
- Capital Markets Law No. 6362, consolidated text, Article 99/A (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.