# Coinbase Advanced Trade API in Python: JWT, orders, limits

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

Coinbase Advanced Trade API Python guide: ES256 JWT signing, tested public and sandbox calls, the official SDK, rate limit facts and what the sandbox misses.

**Short answer:** To use the **Coinbase Advanced Trade API in Python**, create a CDP API key with the ECDSA algorithm and generate a new ES256 JWT for every private request. Each token expires after 2 minutes and is sent as a Bearer token to `api.coinbase.com/api/v3/brokerage`; the official `coinbase-advanced-py` SDK does this for you. Public market data needs no key. The sandbox returns fixed mocked responses, so it tests your request and response handling but not your trading logic.

Two things trip people up on this API: the JWT, which replaces the HMAC signing many exchanges use, and the sandbox, which looks like a testnet but returns canned answers. This guide uses Coinbase's official documentation as read on 4 November 2026; general concepts are in [crypto exchange API integration](https://fincodelabs.com/en/blog/crypto-exchange-api-integration).

**What was run and what was not.** Every sample ran on 4 November 2026 on Python 3.11.3 against public endpoints and the sandbox, with a throwaway key that was never registered. Apart from one unauthenticated request that shows the 401, nothing reached private production endpoints, so live authentication, real fills and live rate-limit responses are untested here.

This is a technical guide, not a recommendation to use Coinbase or any strategy, and availability depends on your jurisdiction and Coinbase's terms. For readers in Turkey, Article 99/A of Capital Markets Law No. 6362 treats activity by foreign-based crypto-asset platforms directed at Turkish residents as unauthorized service provision, so foreign platforms may not target them. Automated trading can lose money.

## Coinbase API Python setup: key, SDK and hosts

You need one CDP API key, either the official SDK or your own JWT code, and the right host for each market.

- **Key.** Create it in the Coinbase Developer Platform portal. Coinbase's authentication page says to select ECDSA and that Ed25519 is not supported and may fail, yet the official SDK's README calls Ed25519 recommended, and SDK 1.8.4 built an EdDSA token offline. The samples here use ECDSA; neither type was tested against a private endpoint. The steps are also in the [exchange API key guide](https://fincodelabs.com/en/blog/exchange-api-key-how-to).
- **Permissions.** A key carries `view`, `trade` or `transfer`. `POST /orders` needs `trade`; `GET /accounts` and `POST /orders/preview` need only `view`. Leave `transfer` off unless the software must move funds; [API key security for trading bots](https://fincodelabs.com/en/blog/api-key-security-trading-bots) explains why.
- **SDK.** `coinbase-advanced-py` is Coinbase's official Python package. The stable PyPI release on 4 November 2026 was 1.8.4 (19 June 2026); 2.0.0rc1 (30 September 2026) is a release candidate that adds Global Derivatives clients and needs Python 3.10 or later.

**Advanced Trade hosts, from Coinbase's REST and WebSocket pages, 4 November 2026**

| Use | Host |
| --- | --- |
| Spot and US derivatives REST | `https://api.coinbase.com/api/v3/brokerage` |
| Market data WebSocket | `wss://advanced-trade-ws.coinbase.com` |
| User order WebSocket | `wss://advanced-trade-ws-user.coinbase.com` |
| Static sandbox | `https://api-sandbox.coinbase.com/api/v3/brokerage` |
| Global Derivatives (JSON-RPC 2.0) | `https://drb.coinbase.com/api/v2` |

FIX is not an option here: Coinbase's FAQ says it is not supported on Advanced Trade.

## Coinbase API Python example: public market data

Endpoints under `/market` and `/time` need no key, so this sample runs as it is.

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

BASE = "https://api.coinbase.com/api/v3/brokerage"

def floor_to(value, step):
    step = Decimal(step)
    return (Decimal(value) / step).to_integral_value(rounding=ROUND_DOWN) * step

print(requests.get(f"{BASE}/time", timeout=10).json()["iso"])
p = requests.get(f"{BASE}/market/products/BTC-USD", timeout=10).json()
print(p["status"], p["price"], p["price_increment"], p["base_increment"], p["quote_min_size"])
print(floor_to(Decimal(p["price"]) * Decimal("0.95"), p["price_increment"]))   # hypothetical limit
print(requests.get(f"{BASE}/accounts", timeout=10).status_code)                # private, no JWT
```

Output on 4 November 2026:

```
2026-11-04T19:50:05Z
online 79993.73 0.01 0.00000001 1
75994.04
401
```

The last line shows that a private endpoint without a JWT returns HTTP 401. Coinbase's REST page says public responses are cached for 1 second; for fresher data it points to the WebSocket or a `cache-control: no-cache` header.

> **Hypothetical example.** A rule sets a limit 5% below a last price of 79,993.73 USD, which gives 75,994.0435. With a `price_increment` of 0.01 it floors to 75,994.04. This shows the rounding only.

## How to build the Coinbase JWT in Python

Each private REST request needs its own ES256 JWT whose `uri` claim names the method, host and path.

```python
import secrets, time
import jwt                                         # pip install PyJWT cryptography
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec

private = ec.generate_private_key(ec.SECP256R1())  # throwaway key, never registered
PEM = private.private_bytes(serialization.Encoding.PEM,
                            serialization.PrivateFormat.TraditionalOpenSSL,
                            serialization.NoEncryption()).decode()
KEY_NAME = "organizations/demo-org/apiKeys/demo-key"

def build_jwt(method, path, host="api.coinbase.com"):
    now = int(time.time())
    claims = {"sub": KEY_NAME, "iss": "cdp", "nbf": now, "exp": now + 120,
              "uri": f"{method} {host}{path}"}     # path only, no query string
    return jwt.encode(claims, PEM, algorithm="ES256",
                      headers={"kid": KEY_NAME, "nonce": secrets.token_hex()})

token = build_jwt("GET", "/api/v3/brokerage/accounts")
header = jwt.get_unverified_header(token)
claims = jwt.decode(token, private.public_key(), algorithms=["ES256"])
print(header["alg"], header["kid"])
print(claims["iss"], claims["uri"], claims["exp"] - claims["nbf"])
print(token == build_jwt("GET", "/api/v3/brokerage/accounts"))   # False: new nonce, new signature
```

Output on 4 November 2026:

```
ES256 organizations/demo-org/apiKeys/demo-key
cdp GET api.coinbase.com/api/v3/brokerage/accounts 120
False
```

The token was checked locally and never sent to production. The SDK's `jwt_generator` (1.8.4) produced the same fields. Send the result as `Authorization: Bearer <token>`.

If a private call returns 401, check these in order:

- **Algorithm.** This code signs with ES256, so it needs an ECDSA key, not Ed25519.
- **PEM format.** Keep the whole key text; real newlines and the `\n` escapes shown in the SDK's README both loaded in our test.
- **Reuse.** A token expires after 2 minutes, and Coinbase asks for a different one for each request.
- **The `uri` claim.** The SDK builds it from method, host and path, with no query string, and a wrong host or path breaks it.
- **Clock.** `nbf` and `exp` come from your machine, so keep it synchronized.

## Coinbase Advanced Trade API sandbox: what it tests

The sandbox needs no authentication and returns fixed responses that Coinbase says match production, so it tests your parsing and error paths, not your order logic.

```python
import requests

SANDBOX = "https://api-sandbox.coinbase.com/api/v3/brokerage"
order = {"client_order_id": "demo-0001", "product_id": "BTC-USD", "side": "BUY",
         "order_configuration": {"limit_limit_gtc": {"base_size": "0.0002",
                                                     "limit_price": "10000.00"}}}

ok = requests.post(f"{SANDBOX}/orders", json=order, timeout=10)
fail = requests.post(f"{SANDBOX}/orders", json=order, timeout=10,
                     headers={"X-Sandbox": "PostOrder_insufficient_fund"})
sent = ok.json()["success_response"]
cfg = ok.json()["order_configuration"]["limit_limit_gtc"]
print(ok.status_code, ok.json()["success"], sent["side"], sent["client_order_id"],
      cfg["base_size"], cfg["limit_price"])
print(fail.status_code, fail.json()["success"], fail.json()["error_response"]["error"])
print(requests.get(f"{SANDBOX}/market/products/BTC-USD", timeout=10).status_code)
```

Output on 4 November 2026:

```
200 True SELL sandbox_success_order 1 70000.44
200 False INSUFFICIENT_FUND
404
```

The request asked to buy 0.0002 BTC at 10,000 USD. The sandbox answered with a sell of 1 BTC at 70,000.44 and the client order ID `sandbox_success_order`, ignoring what was sent, so assertions on returned values prove nothing. A rejected order also came back as HTTP 200 with `success` false, so read the body, not just the status. The reference marks `error` deprecated and requires `new_order_failure_reason`, which the sandbox omitted. Market data returned 404.

**Sandbox error triggers, from Coinbase's sandbox page, 4 November 2026**

| Endpoint | X-Sandbox header | Error returned |
| --- | --- | --- |
| `POST /orders` | `PostOrder_insufficient_fund` | `INSUFFICIENT_FUND` |
| `POST /orders/batch_cancel` | `CancelOrders_failure` | `UNKNOWN_CANCEL_ORDER` |
| `POST /orders/edit` | `EditOrder_failure` | `ORDER_NOT_FOUND` |
| `POST /orders/edit_preview` | `PreviewEditOrder_failure` | `ORDER_NOT_FOUND` |
| `POST /orders/preview` | `PreviewOrder_insufficient_fund` | `PREVIEW_INSUFFICIENT_FUND` |

All five triggers returned these errors when run. A page note limits the sandbox to Accounts and Orders, yet its table adds Portfolios and Perpetuals, and `GET /portfolios` returned 200. No testnet with live matching appears in the documentation read. A cautious next step is `POST /orders/preview` on production with a view-only key, which was not run here. Any live order needs the limits in [trading bot risk management](https://fincodelabs.com/en/blog/trading-bot-risk-management).

## Placing an order with the Coinbase Python SDK

`RESTClient` signs every request for you, and each order response carries a `success` flag that you must check.

```python
import os
from coinbase.rest import RESTClient               # pip install coinbase-advanced-py

client = RESTClient(api_key=os.environ["CDP_KEY_NAME"], api_secret=os.environ["CDP_PRIVATE_KEY"],
                    timeout=10, base_url="api-sandbox.coinbase.com")   # remove for production
print(len(client.get_accounts().accounts))
order = client.limit_order_gtc_buy(client_order_id="demo-0001", product_id="BTC-USD",
                                   base_size="0.0002", limit_price="10000.00")
if order.success:
    order_id = order.success_response["order_id"]
    print(order_id, client.cancel_orders(order_ids=[order_id]).results[0]["success"])
else:
    print(order.error_response)
```

Output on 4 November 2026, on the sandbox:

```
4
f898eaf4-6ffc-47be-a159-7ff292e5cdcf True
```

The order ID is the sandbox's fixed mock value. Removing `base_url` points the same code at production, which was not run.

- **Client order IDs.** Coinbase's Create Order reference says a repeated `client_order_id` creates nothing and returns the existing order, which makes a retry after a timeout safe if you generate the ID once, before the first send.
- **A confusing field.** The FAQ says `success: true` alongside `failure_reason: UNKNOWN_FAILURE_REASON` means the order was accepted.
- **Accepted is not filled.** Follow the order on the `user` channel or through `GET /orders/historical/fills`.

If you need several exchanges behind one interface, the [CCXT Python guide](https://fincodelabs.com/en/blog/ccxt-python-trading-bot) covers that route.

## Coinbase Advanced Trade API rate limit: what is documented

As of 4 November 2026, Coinbase's current Advanced Trade pages publish a WebSocket limit but no per-second REST figure, so read the response headers and handle HTTP 429 rather than hard-coding a number.

**Rate limit statements found in official pages, 4 November 2026**

| Item | What the page says | Caveat |
| --- | --- | --- |
| WebSocket | Connections and unauthenticated messages: 8 per second per IP each | A Coinbase Business copy says 750 connections per second; design for 8 |
| REST per-second limit | None in any Advanced Trade section of Coinbase's docs, searched in full | The older rate limit URL now redirects to the overview |
| Coinbase App API | 10,000 requests per hour per key; HTTP 429 with `rate_limit_exceeded` | The page does not say whether it covers Advanced Trade endpoints |
| SDK headers | `rate_limit_headers=True` adds `x-ratelimit-limit`, `-remaining` and `-reset` | Public, sandbox and 401 responses returned none in our runs; authenticated ones were not tested |

Per-second figures in older articles are left out because no official page confirms them. In practice, stream market data instead of polling, and back off on 429 as described in the [integration guide](https://fincodelabs.com/en/blog/crypto-exchange-api-integration).

## Coinbase WebSocket in Python: market data and order updates

Most market data channels need no key in the current docs, but you must subscribe within 5 seconds of connecting or the server disconnects you.

```python
import asyncio, json
import websockets                                  # pip install websockets

async def main():
    async with websockets.connect("wss://advanced-trade-ws.coinbase.com") as ws:
        await ws.send(json.dumps({"type": "subscribe", "channel": "heartbeats"}))
        await ws.send(json.dumps({"type": "subscribe", "channel": "ticker",
                                  "product_ids": ["BTC-USD"]}))
        while True:
            msg = json.loads(await ws.recv())
            for event in msg.get("events", []):
                if msg["channel"] == "ticker" and event["type"] == "update":
                    t = event["tickers"][0]
                    print(t["product_id"], t["best_bid"], t["best_ask"])
                    return

asyncio.run(main())
```

Output on 4 November 2026:

```
BTC-USD 79980.26 79981.75
```

Each channel needs its own subscribe message. Coinbase's endpoint page says most channels close within 60 to 90 seconds when no updates arrive, and that `heartbeats` keeps the connection open. Building a depth book from `level2` follows the logic in the [local order book guide](https://fincodelabs.com/en/blog/websocket-order-book).

Order updates arrive on the `user` channel at the user host, which requires a fresh JWT in each subscribe message. It was not run, because it needs a real key.

## Global Derivatives moved: what changed on 1 October 2026

International perpetuals no longer trade through the Advanced Trade REST API: since 1 October 2026 they run on a Deribit-powered JSON-RPC 2.0 gateway at `drb.coinbase.com/api/v2`, and the `/intx/` endpoints are retired, apart from reads of older history for a limited period.

Spot and US futures stay on `api.coinbase.com`, and existing CDP keys still work: `public/auth` exchanges a CDP JWT for an access token.

A `public/get_time` call returned a normal JSON-RPC result without a key on 4 November 2026; nothing else on the gateway was run. Derivatives add leverage and liquidation risk, and nothing here suggests using them.

## Frequently asked questions

### Is there a Coinbase Advanced Trade testnet?

No testnet with live matching appears in the official documentation read on 4 November 2026. The sandbox at api-sandbox.coinbase.com returns fixed mocked responses, without authentication.

### Which key type does the Coinbase Advanced Trade API need?

Coinbase's authentication page says ECDSA, which signs ES256 JWTs, and that Ed25519 is not supported; the official SDK's README calls Ed25519 recommended. Neither was tested with a real key here.

### What is the Coinbase Advanced Trade API rate limit?

The WebSocket page read on 4 November 2026 gives 8 per second per IP for connections and unauthenticated messages. No per-second REST figure appeared on the current pages, so read the x-ratelimit response headers and handle HTTP 429.

### Can I use an API key to trade for other people?

Coinbase's authentication page limits API key authentication to your own account and points to OAuth2 for other users' accounts. Trading for others can also require a license, depending on the jurisdiction.

## Sources

- [Coinbase: Welcome to Advanced Trade API](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/overview)
- [Coinbase: Advanced Trade REST Endpoints (hosts, public caching, retired INTX)](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/rest-api)
- [Coinbase: API key authentication (ECDSA, JWT claims, expiry)](https://docs.cdp.coinbase.com/coinbase-app/authentication-authorization/api-key-authentication)
- [Coinbase: Advanced Trade API Sandbox](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/sandbox)
- [Coinbase: Advanced Trade Scope and Permissions](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/rest-scopes)
- [Coinbase: Create Order reference (client_order_id)](https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/orders/create-order)
- [Coinbase: Advanced Trade API FAQ (FIX, UNKNOWN_FAILURE_REASON)](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/faq)
- [Coinbase: Advanced Trade WebSocket Overview](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/websocket/websocket-overview)
- [Coinbase: Advanced Trade WebSocket Endpoints (channels, heartbeats)](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/websocket/websocket-endpoints)
- [Coinbase: Advanced Trade WebSocket Rate Limits](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/websocket/websocket-rate-limits)
- [Coinbase Business: Advanced Trade WebSocket Rate Limits](https://docs.cdp.coinbase.com/coinbase-business/advanced-trade-apis/websocket/websocket-rate-limits)
- [Coinbase: Coinbase App Rate Limiting](https://docs.cdp.coinbase.com/coinbase-app/api-architecture/rate-limiting)
- [Coinbase: Global Derivatives Overview (1 October 2026 migration)](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/guides/derivatives/overview)
- [GitHub: coinbase-advanced-py README and CHANGELOG (official Python SDK, 1.8.4 and 2.0.0rc1)](https://github.com/coinbase/coinbase-advanced-py)
- [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/coinbase-advanced-trade-api-python

Cite as: Fincode, "Coinbase Advanced Trade API in Python: JWT, orders, limits", updated November 5, 2026, https://fincodelabs.com/en/blog/coinbase-advanced-trade-api-python
