# Deribit API Python: testnet, API keys and rate limits

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

Deribit API in Python, tested on test.deribit.com: JSON-RPC over HTTP and WebSocket, API keys and scopes, heartbeats, book sync and credit limits.

**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](https://fincodelabs.com/en/blog/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](https://fincodelabs.com/en/blog/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.

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

| 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 `testnet` flag.
- **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`.

```python
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_instruments` costs 10,000 credits** and is cached for about a minute. Only state `open` takes orders; `locked` allows cancels only. In checks from 20:18 to 21:05 UTC, every BTC entry on the test host was locked or halted and `public/status` returned `locked: true`, yet the ticker said `open`.
- **Names seen:** `BTC-PERPETUAL`, `BTC-6NOV26` (future), `BTC-6NOV26-70000-C` (option). Prices must sit on the tick (options add `tick_size_steps`) or orders fail with 10043. Ticker funding fields are explained in [perpetual futures funding rate](https://fincodelabs.com/en/blog/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 `:read` or `:read_write`. `wallet:read_write` allows withdrawals, which a trading bot does not need. Add an IP whitelist and check `scope` in the auth reply, since the server may grant less. See [API key security for trading bots](https://fincodelabs.com/en/blog/api-key-security-trading-bots).

**public/auth grant types, per Deribit's authentication article**

| 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:

```python
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:

```python
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.100ms` sent nothing in three 8-second tries.
- **Silent failures:** a misspelled channel returned an empty list, no error, hence `wanted`. The `raw` interval 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](https://fincodelabs.com/en/blog/websocket-heartbeat-reconnect) and [local order book sync](https://fincodelabs.com/en/blog/websocket-order-book).

## 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.

**Credit costs on Deribit's rate limits page, read 5 November 2026**

| 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 |

**Matching-engine limits (orders, edits, cancels) by trailing 7-day volume, updated hourly**

| 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-matching `burst` 1,500 and `rate` 1,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.

**Codes returned by test.deribit.com on 5 November 2026**

| 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](https://fincodelabs.com/en/services/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)](https://docs.deribit.com/)
- [Deribit API: Quickstart Guide](https://docs.deribit.com/articles/deribit-quickstart)
- [Deribit API: JSON-RPC 2.0 protocol overview (transports, testnet field, no batching)](https://docs.deribit.com/articles/json-rpc-overview)
- [Deribit API: Authentication with OAuth 2.0 and signatures](https://docs.deribit.com/articles/authentication)
- [Deribit API: Creating a new API key](https://docs.deribit.com/articles/creating-api-key)
- [Deribit API: Access scope](https://docs.deribit.com/articles/access-scope)
- [Deribit API: Rate limits (credit system and matching-engine tiers)](https://docs.deribit.com/articles/rate-limits)
- [Deribit API: Connection management (limits, heartbeats, cancel on disconnect)](https://docs.deribit.com/articles/connection-management-best-practices)
- [Deribit API: Notifications and subscriptions (book snapshots and change IDs)](https://docs.deribit.com/articles/notifications)
- [Deribit API reference: public/get_instruments (cost, caching, states)](https://docs.deribit.com/api-reference/market-data/public-get_instruments)
- [Deribit API: API usage policy (heartbeat frequency, errors and IP bans)](https://docs.deribit.com/articles/api-usage-policy)
- [Deribit API: Error codes](https://docs.deribit.com/articles/errors)
- [Deribit Support: Restricted Jurisdictions (updated 6 October 2026)](https://support.deribit.com/hc/en-us/articles/25944487427741-Restricted-Jurisdictions)
- [Deribit Support: Terms of Service, DRB Panama Inc. (version 2 February 2026)](https://support.deribit.com/hc/en-us/articles/25944471089437-Terms-of-Service-DRB-Panama-Inc)
- [Capital Markets Law No. 6362, consolidated text, Article 99/A (Turkish)](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/deribit-api-python

Cite as: Fincode, "Deribit API Python: testnet, API keys and rate limits", updated November 6, 2026, https://fincodelabs.com/en/blog/deribit-api-python
