# Bitvavo API Python: signing, weight limits and error codes

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

Bitvavo API in Python, read 6 Nov 2026: HMAC signing, operatorId, the 1,000-point weight limit, error codes, the WebSocket order book and tested code.

**Short answer:** To use the **Bitvavo API in Python**, sign each private request with HMAC-SHA256 over timestamp, method, path and body, and send it to `https://api.bitvavo.com/v2` with four `Bitvavo-Access` headers. Create, update and cancel calls also need an integer `operatorId`. As read on 6 November 2026, the default limit is 1,000 weight points a minute, and going over it returns HTTP 429 with errorCode 105.

Facts come from Bitvavo's official docs, specs and FAQ, read on 6 November 2026 (Istanbul time); code ran that day on Python 3.11.3 with requests 2.31.0. The docs list no sandbox, so signed calls were tested against a local mock server.

This is a technical guide, not a recommendation to use Bitvavo or any strategy, and automated trading can lose money. ESMA's interim MiCA register (updated 5 November 2026) lists Bitvavo B.V. as authorized by the Dutch AFM on 26 June 2025 for custody, operating a trading platform and transfer services. Your country's rules and Bitvavo's terms still decide what you may do. Article 99/A of Capital Markets Law No. 6362 treats activity by foreign crypto-asset platforms directed at Turkish residents as unauthorized.

## Bitvavo API key: permissions and IP whitelist

Create a key in your Bitvavo account under Settings, API tab, after enabling two-factor authentication; the secret is shown only once.

- **View access**, called Read-only in the REST introduction: balances and transactions.
- **Trade digital assets**: create, update and cancel orders. Reading orders and trades needs both permissions.
- **Withdraw digital assets**: API withdrawals skip 2FA and email confirmation, so a trading bot should not hold it.

Whitelist your server's IP address: calls from elsewhere get errorCode 307, and missing permissions 310 to 312. Storage and rotation are in [API key security for trading bots](https://fincodelabs.com/en/blog/api-key-security-trading-bots).

## How Bitvavo API authentication works

Each private REST call carries four headers, and the signature is a hex HMAC-SHA256, keyed with your secret, of timestamp + method + path + body.

The headers are `Bitvavo-Access-Key` (64 characters per the spec), `Bitvavo-Access-Timestamp` (Unix milliseconds), `Bitvavo-Access-Signature` and the optional `Bitvavo-Access-Window`: default 10,000 ms, maximum 60,000 and, per errorCode 303, minimum 100.

- The path includes `/v2`. With the secret `bitvavo`, the docs' order example signs to `44d02272...`, which the code below reproduces.
- GET signs an empty body; otherwise sign and send the same string. The official SDK signs compact JSON but sends it through requests' `json=`, which adds spaces. A mock checking raw bytes rejected that; Bitvavo's own behavior could not be tested without a key.
- The introduction says path; the spec says path and parameters, and cancels put `operatorId` in the query. The SDK signs `/v2/orders?market=BTC-EUR` with the query, as the client below does; confirm it with a read-only key.
- Requests outside the window fail with errorCode 304. The test machine ran about 1.3 seconds behind `/time` on 6 November 2026.
- On the WebSocket, send the `authenticate` action with a signature of timestamp + `GET` + `/v2/websocket`.

The general method is in API signature generation.

## Bitvavo API rate limit: 1,000 weight points a minute

Every REST request and WebSocket action costs weight from a default 1,000 points a minute, counted across your accounts with a key and per IP address without one.

REST responses carry `bitvavo-ratelimit-limit`, `-remaining` and `-resetat` (milliseconds); WebSocket responses carry none. Going over returns HTTP 429 with errorCode 105; with a key, the docs' example is blocked from 12:00:15 to 12:02:00, and without one for 15 minutes by IP.

**Weights in the OpenAPI spec, read 6 November 2026**

| Request | Weight |
| --- | --- |
| Order book, candles, ticker price and book, time, markets, assets | 1 |
| Ticker 24h | 1 with `market`, 25 without |
| Create, update, get or cancel one order | 1 |
| Public trades, trade history, orders, balance | 5 |
| Open orders | 5 with `market`, 100 without |
| Cancel orders | 25 with `market`, 100 without |
| Atomic cancel orders | 100 |

On 19 May 2026 open orders went from 1 and 25 to 5 and 100, and cancels started costing weight.

**Cached responses carry stale headers.** On 6 November 2026, candles responses came from Cloudflare's cache (`cf-cache-status: HIT`, `Age` up to 28 seconds); one showed 988 points remaining and a `resetat` already past, while `/time` showed 998 after the reset. Hits did not appear to spend weight. Consecutive weight-1 calls lowered the count by 0, 1 or 2, so keep a reserve.

> **Hypothetical example.** Polling open orders for all markets every 5 seconds costs 12 x 100 = 1,200 points a minute, more than the whole budget; three named markets cost 12 x 3 x 5 = 180.

## Bitvavo API error codes that need different handling

Check the HTTP status before the errorCode, because errorCode 429 is a decimals error inside an HTTP 400, not a rate limit.

**Selected codes from Bitvavo's Handle errors page, read 6 November 2026**

| HTTP | errorCode | Meaning | Handling |
| ---: | --- | --- | --- |
| 400 | 429 | Too many decimal places | Fix the value; waiting does nothing |
| 400 | 422 | Price off the tick size | Use `tickSize` from `/markets` |
| 403 | 304, 309 | Outside the window; bad signature | Sync the clock; check the signed string |
| 429 | 105 | Rate limit, blocked | Wait until `resetat` |
| 503 | 107, 108 | Overloaded or processing issue | Retry after 500 ms |
| 503 | 109 | Timed out; may have succeeded | Get the order before resending |

The docs advise retrying 500 after 500 ms, 1 s, 2 s and so on, 503 after 500 ms, and never 400, 403 or 404 (see [exponential backoff with jitter](https://fincodelabs.com/en/blog/exponential-backoff-jitter-python)); after 109, look the order up by its [client order ID](https://fincodelabs.com/en/blog/client-order-id). One conflict: the spec and errorCode 235 say 100 open orders per market, but `/markets` returned `maxOpenOrders` 400 for BTC-EUR on 6 November 2026; which applies was not testable without a key, so handle errorCode 235.

## Bitvavo API Python example, tested

The client signs as documented, sends the bytes it signed, takes weight only from uncached responses and validates orders before any network call.

```python
import hashlib, hmac, json, time
from decimal import Decimal
from urllib.parse import urlencode
import requests

HOST = "https://api.bitvavo.com"

class BitvavoError(Exception):
    def __init__(self, status, code, text):
        super().__init__(f"HTTP {status}, errorCode {code}: {text}")
        self.status, self.code = status, code

def sign(secret, ts, method, path, body=""):
    msg = f"{ts}{method}{path}{body}".encode()
    return hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()

def dec(value):                              # "7.16E+4" is 71600; inf is refused
    d = Decimal(str(value)).normalize()
    if not d.is_finite():
        raise ValueError(f"not a finite number: {value}")
    return d

def limit_body(info, side, amount, price, operator_id, client_id=None):
    if not (type(operator_id) is int and (client_id is None or client_id) and info.get("status") == "trading"):
        raise ValueError("operatorId not an integer, empty clientOrderId or market not trading")
    try:
        a, p = dec(amount), dec(price)
        off_tick = p <= 0 or bool(p % dec(info["tickSize"]))
    except ArithmeticError:                  # "abc", or "1e400" % 1 overflowing
        raise ValueError(f"unusable number: {amount}, {price}") from None
    size_ok = dec(info["minOrderInBaseAsset"]) <= a <= dec(info["maxOrderInBaseAsset"])
    if off_tick or not size_ok or -a.as_tuple().exponent > info["quantityDecimals"]:
        raise ValueError(f"{amount} at {price} breaks the tick size, size limits or decimals")
    body = {"market": info["market"], "side": side, "orderType": "limit",
            "amount": format(a, "f"), "price": format(p, "f"), "operatorId": operator_id}
    if client_id:
        body["clientOrderId"] = client_id
    return body

class Bitvavo:
    def __init__(self, key="", secret="", window=10000, reserve=50, http=None):
        self.key, self.secret, self.window, self.reserve = key, secret, window, reserve
        self.http, self.remaining, self.reset_ms = http or requests.Session(), None, 0

    def call(self, method, path, params=None, body=None, private=False):
        if private and not (self.key and self.secret):
            raise BitvavoError(None, 300, "no key configured, nothing sent")
        wait = self.reset_ms / 1000 - time.time()
        if wait > 0 and self.remaining is not None and self.remaining <= self.reserve:
            raise BitvavoError(None, 105, f"local pause, {wait:.0f} s to reset")
        query = urlencode(params or {})
        url = "/v2" + path + ("?" + query if query else "")
        data = json.dumps(body, separators=(",", ":")) if body else ""
        headers = {"Content-Type": "application/json"}
        if private:
            ts = int(time.time() * 1000)
            headers.update({"Bitvavo-Access-Key": self.key, "Bitvavo-Access-Timestamp": str(ts),
                            "Bitvavo-Access-Signature": sign(self.secret, ts, method, url, data),
                            "Bitvavo-Access-Window": str(self.window)})
        r = self.http.request(method, HOST + url, data=data or None, headers=headers, timeout=10)
        h = r.headers
        cached = h.get("cf-cache-status") == "HIT" or "Age" in h
        if "bitvavo-ratelimit-remaining" in h and not cached:
            self.remaining = int(h["bitvavo-ratelimit-remaining"])
            self.reset_ms = int(h.get("bitvavo-ratelimit-resetat", 0))
        if r.status_code == 429:             # blocked up to 2 min with a key, 15 by IP
            self.remaining = 0
            self.reset_ms = max(self.reset_ms, (time.time() + (120 if private else 900)) * 1000)
        try:
            payload = r.json()
        except ValueError:                   # e.g. an HTML proxy page
            raise BitvavoError(r.status_code, None, r.text[:60]) from None
        if r.status_code >= 400:
            err = payload if isinstance(payload, dict) else {}
            raise BitvavoError(r.status_code, err.get("errorCode"), err.get("error", ""))
        return payload
```

The demonstration:

```python
from bitvavo_client import *

doc_body = '{"market":"BTC-EUR","side":"buy","price":"5000","amount":"1.23","orderType":"limit"}'
print(sign("bitvavo", 1548172481125, "POST", "/v2/order", doc_body)[:16])  # docs: 44d022723a20973a
api = Bitvavo()                              # public endpoints need no key
print("server time", api.call("GET", "/time")["time"], "remaining", api.remaining)
info = api.call("GET", "/markets", {"market": "BTC-EUR"})
print(info["tickSize"], info["quantityDecimals"], info["minOrderInBaseAsset"], info["maxOpenOrders"])
for price, amount in [(float("1e400"), "0.001"), ("60000", "1e-400"), ("60000.5", "0.001"), ("6E+4", "0.00100")]:
    try:
        print("ok", limit_body(info, "buy", amount, price, operator_id=2001))
    except ValueError as err:
        print("refused:", err)

class Fake:                                  # synthetic error response, no network
    def __init__(self, status, code):
        self.status_code, self.headers = status, {}
        self.text = json.dumps({"errorCode": code, "error": "synthetic"})
    def json(self):
        return json.loads(self.text)

for status, code in [(400, 429), (429, 105)]:
    api = Bitvavo("k" * 64, "secret", http=type("H", (), {"request": lambda *a, **k: Fake(status, code)})())
    for attempt in range(2):
        try:
            api.call("GET", "/balance", private=True)
        except BitvavoError as err:
            print(attempt, err)
```

Output on 6 November 2026:

```
44d022723a20973a
server time 1793921637418 remaining 999
1.00 8 0.00007294 400
refused: not a finite number: inf
refused: 1e-400 at 60000 breaks the tick size, size limits or decimals
refused: 0.001 at 60000.5 breaks the tick size, size limits or decimals
ok {'market': 'BTC-EUR', 'side': 'buy', 'orderType': 'limit', 'amount': '0.001', 'price': '60000', 'operatorId': 2001}
0 HTTP 400, errorCode 429: synthetic
1 HTTP 400, errorCode 429: synthetic
0 HTTP 429, errorCode 105: synthetic
1 HTTP None, errorCode 105: local pause, 120 s to reset
```

Decimal also flattens exponent notation, which Bitvavo sends: that day the all-markets 24-hour ticker had 16 such fields in 7 of 437 markets, such as a BTC-EUR `high` of `7.184E+4`. Separate tests passed against a local mock that checks signatures over the raw body and query (a wrong secret got 309), and for an amount of `"1e400"`, an empty `clientOrderId`, `True` or `"2001"` as `operatorId`, HTML error pages and error bodies without fields.

## Bitvavo WebSocket API and the local order book

Connect to `wss://ws.bitvavo.com/v2/`, subscribe to `book`, take a `getBook` snapshot and apply updates only while each `nonce` is exactly one higher than the last.

```json
{"action": "subscribe", "channels": [{"name": "book", "markets": ["BTC-EUR"]}]}
{"action": "getBook", "market": "BTC-EUR", "depth": 1000, "requestId": 7}
```

Key price levels by Decimal, not by string. On 6 November 2026 the snapshot wrote `"67508"` and updates `"67508.00"`; in a 25-second run with 246 updates after a snapshot of 1,000 levels a side, a string-keyed book ended with 2,155 levels against 2,018 for a Decimal-keyed one, and its best bid equaled its best ask.

- The nonce resets when the matching engine restarts (FAQ): resync on any gap.
- Ticker events carried only the side that changed, such as `bestAsk` alone.
- Order and fill events have no ordering guarantee.
- A session may send 5,000 messages a second before errorCode 112, and all connections share one weight budget.

See [WebSocket heartbeat and reconnect](https://fincodelabs.com/en/blog/websocket-heartbeat-reconnect) and [maintaining a local order book](https://fincodelabs.com/en/blog/websocket-order-book).

## Official Bitvavo Python SDK: what to check first

The official `python-bitvavo-api` (ISC license) covers the core calls but has not followed the API since mid-2025: PyPI's newest version is 1.4.3, uploaded 14 May 2025, and the last push was 4 August 2025.

- No methods for cancel on disconnect, atomic cancel or the May 2026 `/crypto/withdrawal`.
- `cancelOrder` lets you omit `operatorId`, which the API has rejected since 30 June 2025.
- It parses the errorCode 105 reset time from the message text and trusts headers on cached responses.

Test against a mock first, as in [paper trading APIs](https://fincodelabs.com/en/blog/paper-trading-api). For integration work in general, see Fincode's [exchange API integration](https://fincodelabs.com/en/services/exchange-api-integration) page; naming Bitvavo here does not mean Fincode integrates it or has built on it.

## Frequently asked questions

### Does Bitvavo have a sandbox or testnet?

None was found in the official docs or specs on 6 November 2026; the specs list production servers only.

### What is operatorId in the Bitvavo API?

An integer you choose for the trader or algorithm behind an order. Since 30 June 2025 the API rejects create, update and cancel requests without it.

### How do I see my remaining Bitvavo API weight?

Read the bitvavo-ratelimit-remaining header on uncached REST responses; WebSocket responses do not carry it.

### Is errorCode 429 a Bitvavo rate limit?

No. It comes with HTTP 400 and means too many decimal places. The rate limit is HTTP 429 with errorCode 105.

## Sources

- [Bitvavo API Docs: Exchange REST API introduction, authentication and permissions](https://docs.bitvavo.com/docs/rest-api/introduction/)
- [Bitvavo API Docs: Rate limits](https://docs.bitvavo.com/docs/rate-limits/)
- [Bitvavo API Docs: Handle errors](https://docs.bitvavo.com/docs/errors/)
- [Bitvavo API Docs: Get started (API key creation and permissions)](https://docs.bitvavo.com/docs/get-started/)
- [Bitvavo API Docs: Exchange WebSocket API introduction](https://docs.bitvavo.com/docs/websocket-api/introduction/)
- [Bitvavo API Docs: Manage local order book](https://docs.bitvavo.com/docs/manage-order-book/)
- [Bitvavo API Docs: Frequently asked questions](https://docs.bitvavo.com/docs/faqs/)
- [Bitvavo API Docs: Releases changelog](https://docs.bitvavo.com/releases/)
- [Bitvavo Exchange REST API OpenAPI specification (weights, servers)](https://docs.bitvavo.com/api-specs/exchange-rest-api.yaml)
- [Bitvavo on GitHub: python-bitvavo-api, the official Python SDK](https://github.com/bitvavo/python-bitvavo-api)
- [Bitvavo public endpoint: market data for BTC-EUR (live)](https://api.bitvavo.com/v2/markets?market=BTC-EUR)
- [ESMA: Markets in Crypto-Assets Regulation and the interim MiCA register (updated 5 November 2026)](https://www.esma.europa.eu/esmas-activities/digital-finance-and-innovation/markets-crypto-assets-regulation-mica)
- [ESMA interim MiCA register: authorised crypto-asset service providers (CSV)](https://www.esma.europa.eu/sites/default/files/2024-12/CASPS.csv)
- [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/bitvavo-api-python

Cite as: Fincode, "Bitvavo API Python: signing, weight limits and error codes", updated November 6, 2026, https://fincodelabs.com/en/blog/bitvavo-api-python
