3. Authentication: signing every request with HMAC
Level 13: Connecting to Real Brokers
Connecting to a trading API with Python typically consists of using the requests library to make HTTP requests to the API endpoint. The first thing you need is credentials (API key, secret, and on some exchanges a passphrase) obtained from the platform. Always consult the broker's official documentation for the specific details of endpoints, authentication, rate limits and data formats.
On Coinbase Pro you go to the API section of your account and create a new key. You will get three values: API key, API secret and API passphrase. With them you authenticate every request.
As you saw in earlier levels, those credentials are never written inside the code. They are read from environment variables, and the file that defines them stays out of version control:
import os
import time
import hmac
import hashlib
import base64
import json
import requests
# Defined outside the code, for example in a .env file that is NOT pushed to git:
# export CB_API_KEY='your_key_here'
# export CB_API_SECRET='your_secret_here'
# export CB_API_PASS='your_passphrase_here'
API_KEY = os.environ['CB_API_KEY']
API_SECRET = os.environ['CB_API_SECRET']
API_PASS = os.environ['CB_API_PASS']If the variable does not exist, os.environ[...] raises a KeyError and the program dies on startup. That is exactly what you want: it is far better to fail at launch than to discover mid-session that the credentials were empty.
How the signature works
The secret key never travels across the network. What travels is a *signature*: a cryptographic digest computed with the secret over the exact data of this request. The server, which knows the same secret, repeats the computation and compares. If they match, the request is authentic and nobody has modified it along the way.
# Timestamp in seconds
timestamp = str(time.time())
method = 'POST'
request_path = '/orders'
body_json = json.dumps(body)
# The message to sign: timestamp + method + path + body, in this exact order
message = timestamp + method + request_path + body_json
# The Coinbase secret comes base64-encoded: it must be decoded before use
hmac_key = base64.b64decode(API_SECRET)
signature = hmac.new(hmac_key, message.encode('utf-8'), hashlib.sha256)
signature_b64 = base64.b64encode(signature.digest()).decode('utf-8')
headers = {
'CB-ACCESS-KEY': API_KEY,
'CB-ACCESS-PASSPHRASE': API_PASS,
'CB-ACCESS-SIGN': signature_b64,
'CB-ACCESS-TIMESTAMP': timestamp,
'Content-Type': 'application/json'
}There are three details that cause 90% of beginners' 401 errors:
- The order and the content of the message are literal. If you sign
body_jsonbut then send a regenerated JSON with a different key order or different whitespace, the signature will not match. Serialize once and send exactly that string. - The timestamp matters. It exists to prevent replay attacks: the server rejects requests whose timestamp is too far from its own (typically a few seconds). If your server's clock has drifted, all your requests will fail with an authentication error even though the code is correct. Synchronize the clock with NTP.
- The secret is base64-decoded before being used as the key. Using the string as it comes produces a different signature.
And one last measure that is pure operational common sense: when you create the key, grant it only read and trading permissions, never withdrawal permission, and restrict its use to your server's IP if the exchange allows it. If that key ever leaks, the difference between those permissions and full ones is the difference between a scare and a total loss.