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.
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.
- Permissions. A key carries
view,tradeortransfer.POST /ordersneedstrade;GET /accountsandPOST /orders/previewneed onlyview. Leavetransferoff unless the software must move funds; API key security for trading bots explains why. - SDK.
coinbase-advanced-pyis 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.
| 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.
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.
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
\nescapes 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
uriclaim. The SDK builds it from method, host and path, with no query string, and a wrong host or path breaks it. - Clock.
nbfandexpcome 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.
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.
| 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.
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.
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_idcreates 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: truealongsidefailure_reason: UNKNOWN_FAILURE_REASONmeans the order was accepted. - Accepted is not filled. Follow the order on the
userchannel or throughGET /orders/historical/fills.
If you need several exchanges behind one interface, the CCXT Python guide 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.
| 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.
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.
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.
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 (opens in a new tab)
- Coinbase: Advanced Trade REST Endpoints (hosts, public caching, retired INTX) (opens in a new tab)
- Coinbase: API key authentication (ECDSA, JWT claims, expiry) (opens in a new tab)
- Coinbase: Advanced Trade API Sandbox (opens in a new tab)
- Coinbase: Advanced Trade Scope and Permissions (opens in a new tab)
- Coinbase: Create Order reference (client_order_id) (opens in a new tab)
- Coinbase: Advanced Trade API FAQ (FIX, UNKNOWN_FAILURE_REASON) (opens in a new tab)
- Coinbase: Advanced Trade WebSocket Overview (opens in a new tab)
- Coinbase: Advanced Trade WebSocket Endpoints (channels, heartbeats) (opens in a new tab)
- Coinbase: Advanced Trade WebSocket Rate Limits (opens in a new tab)
- Coinbase Business: Advanced Trade WebSocket Rate Limits (opens in a new tab)
- Coinbase: Coinbase App Rate Limiting (opens in a new tab)
- Coinbase: Global Derivatives Overview (1 October 2026 migration) (opens in a new tab)
- GitHub: coinbase-advanced-py README and CHANGELOG (official Python SDK, 1.8.4 and 2.0.0rc1) (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.