Proper REST signatures

I’m seeing a lot of signature related quesstions lately. So I’d like to take a second to just post this example to hopefully catch some of the quesstions before they are asked.

First thing you’ll need is a functional signature method. This requires several inputs, the most important of which is your api key, not the secret key that for whatever reason seems to be a popular choice.

​ ​private​ ​String​ ​signMessage​(​String​ ​timestamp​, ​String​ ​method​, ​String​ ​path​, ​String​ ​body​) ​//​ ENCRYPTED HASH SECURITY SIGNATURE BUILDER
​ ​throws​ ​NoSuchAlgorithmException​, ​InvalidKeyException​ {

​ ​String​ prehash ​=​ timestamp ​+​ method ​+​ path ​+​ body;
​ ​Mac​ sha256_HMAC ​=​ ​Mac​.​getInstance(​"​HmacSHA256​"​);
​ ​byte​ secretDecoded ​=​ ​Base64​.​getDecoder()​.​decode(​this​.​coinbaseAPIKey);
​ ​SecretKeySpec​ secret_key ​=​ ​new​ ​SecretKeySpec​(secretDecoded, ​"​HmacSHA256​"​);
​ sha256_HMAC​.​init(secret_key);
​ ​return​ ​Base64​.​getEncoder()​.​encodeToString(sha256_HMAC​.​doFinal(prehash​.​getBytes()));
​ }

The second most important thing you need to be sure of is your request headers, both content and accept headers be to be set properly as well as the security credentials. Failure to set any of these parameters will result in an invalid signature response from the server.

​ ​String​ timeStamp ​=​ ​Instant​.​now()​.​getEpochSecond() ​+​ ​"​"​;
​ request ​=​ ​HttpRequest​.​newBuilder()
​ .uri(​URI​.​create(​String​.​join(​"​"​, coinbaseProBaseURL, endpoint)))
​ .header(​"​CB-ACCESS-KEY​"​, coinbaseSecretKey)
​ .header(​"​CB-ACCESS-PASSPHRASE​"​, coinbasePassphrase)
​ .header(​"​CB-ACCESS-SIGN​"​, signMessage(timeStamp, ​"​POST​"​, endpoint, requestBody))
​ .header(​"​CB-ACCESS-TIMESTAMP​"​, timeStamp)​.​header(​"​Accept​"​, ​"​application/json​"​)
​ .header(​"​Content-Type​"​, ​"​application/json​"​)
​ .header(​"​Accept​"​, ​"​application/json​"​)
​ .method(​"​POST​"​, ​HttpRequest​.​BodyPublishers​.​ofString(requestBody))
​ .build();

@arood I knew I had this problem myself having the key positions switched. The wording on this page is misleading as to the keys respective positions:

Fantastic post and thank you for the notes on the documents @cleggink - we really appreciate your continued contributions and insights on these topics.

1 Like

I’ve been able to make a signature to place an order, but when I make a signature to cancel an order, I get an invalid signature message as a response, even though the same procedure is used to make the signature.

Has anyone tried to cancel all orders with the DELETE method? If so, have you had success making a valid signature?

One possible issue is body. What is body for canceling all orders?
Is it just {"profile_id": "key"} ?

If you are attempting my method shown here then when not using a body you should pass “” (nothing) and not a null value. This will negate the value in the prehash. I’ve not had a problem with the delete function in this regard.

I finally got it to work. Here’s what I learned:

  • As @cleggink indicated, body must be an empty string.
  • As in placing an order, the signature is made from the Coinbase secret, not from the key.
  • The URL doesn’t need anything after “/orders” – i.e. no “?profile_id=”. The Coinbase API website is a little confusing on this point: deleteorders

All your points are correct. The profile id parameter is never required, or necessary if your not using multiple api keys or accounts.

Been getting invalid signature errors that just started the past week or so. PowerShell functions haven’t changed, so not sure what’s going on:

FUNCTION Base64-Encode($string) {
$conversion = [System.Text.Encoding]::ASCII.GetBytes($string)
RETURN [System.Convert]::ToBase64String($conversion)
}

FUNCTION Base64-Decode($string) {
$conversion = [System.Convert]::FromBase64String($string)
RETURN [System.Text.Encoding]::ASCII.GetString($conversion)
}

FUNCTION hmac($message, $secret) {
$hmacsha = NEW-OBJECT System.Security.Cryptography.HMACSHA256
$hmacsha.key = [Convert]::FromBase64String($secret)
$signature = $hmacsha.ComputeHash([Text.Encoding]::ASCII.GetBytes($message))
$signature = [Convert]::ToBase64String($signature)
RETURN $signature
}

FUNCTION Submit-Request($request) {
$unixEpochStart = GET-DATE -Date “01/01/1970”
$now = GET-DATE
$timestamp = (NEW-TIMESPAN -Start $unixEpochStart -End $now.ToUniversalTime()).TotalSeconds
$timestamp = ([math]::Round($timestamp, 3)).ToString()
$prehash = $timestamp + $request.method.ToUpper() + $request.url + $request.body
$signature_b64 = hmac -message $prehash -secret $request.secret
$header = @{
“CB-ACCESS-KEY” = $request.key
“CB-ACCESS-SIGN” = $signature_b64
“CB-ACCESS-TIMESTAMP” = $timestamp
“CB-ACCESS-PASSPHRASE” = $request.passphrase
“Content-Type” = ‘application/json’
}
$uri = $request.endpoint + $request.url
IF ($request.method.ToUpper() -eq ‘POST’) {
$response = Invoke-RestMethod -Method $request.method -Uri $uri -Headers $header -Body $request.body
}
ELSE {
$response = Invoke-RestMethod -Method $request.method -Uri $uri -Headers $header
}
RETURN $response
}

@Charlatat, I just ran your hmac function in PowerShell, and didn’t get the same result that I got in Javascript or AHK, which gave valid signatures.

It might not matter, but I didn’t understand your hmac function too well. Specifically, I didn’t understand why $hmacsha.key didn’t appear on the next line.

I didn’t originally create these functions, but I’m assuming that the hmac function sets the “key” property of the $hmacsha object, then the next line generates a hash of the $hmacsha object and puts it into the $signature variable.

All calls work successfully, even BUY/SELL, but those are the only ones that generate the invalid signature error (which terminates my script). Any suggestions would be appreciated.

I don’t know PowerShell, but the signature generated by your script didn’t match the signatures generated by other working scripts.

Look at the suggestions in this authentication thread, showing Python and CryptoJS snippets.

The whole Python code I got to work is here.

I prefer AHK, and two AHK experts helped me generate a working sig, which you can see in this thread.

I’m at work so I don’t have the time to con thru your code right now. But if the key you’re referring to is coinbase api key then you need to set that yourself.

Hi @cleggink, this is working for me with the API keys generated from Coinbase Pro, but how will this work with API keys generated through coinbase.com/settings/api? My concern is that when creating API keys on Coinbase (NOT pro) there’s no passphrase, so how can we have a “CB-ACCESS-PASSPHRASE” in the headers? Should this field just be an empty string?

The documentation on this says CB-ACCESS-PASSPHRASE is required, so I’m not sure how this would work without one.

Thank you for the helpful link. :+1:

Hey Everyone, I’m unable to properly sign GET requests with python. I keep getting {“message”:“invalid signature”}. POST requests work. I can create oders. But when I try to replicate the signing process for GET requests I get status code 401.

I’ve read through every post on here about the same thing. I’ve read through the “Authorization and Authentication” section of the docs many times. I mirrored the JS example exactly. I tried copying the functions from the cbpro library. I read through the hmac python library docs. I learned a bunch of stuff about signing and encoding. And I still can’t figure it out. From my perspective I feel like I’m doing everything right, but that’s obviously not true.

If anyone can see what I’m doing wrong please let me know. I’ve tried as many different things as I can think of. I’ve rewritten the functions many different ways. Still no luck. Pretty much this exact same function works for POST.

passphrase = "xxx"
api_key = "xxx"
secret = "xxx"
current_time = str(int(time.time()))

def get_fills():
    method = "GET"
    path = "/fills"
    payload = {
        "order_id": "xxx"
    }
    body = json.dumps(payload)

    #create the prehash string by concatenating required parts
    message = "{}{}{}{}".format(current_time, method, path, body)
    # decode the base64 secret
    key = base64.b64decode(secret)
    # base64 encode the message
    msg = message.encode('utf-8')
    # create a sha256 hmac with the secret and sign the require message with the hmac
    hmac_hash = hmac.digest(key, msg, hashlib.sha256)
    # base64 encode the result
    sign = base64.b64encode(hmac_hash).decode('utf-8')

    headers = {
        "Accept": "application/json",
        'Content-Type': 'application/json',
        'CB-ACCESS-TIMESTAMP': current_time,
        'CB-ACCESS-KEY': api_key,
        'CB-ACCESS-PASSPHRASE': passphrase,
        'CB-ACCESS-SIGN': sign
        }
    url = "https://api.exchange.coinbase.com/fills"
    response = requests.get(url, body, headers=headers)

Hi @Mike! order_id is a query param in the endpoint: Get all fills. It seems that you are passing it in the JSON body which is the wrong way to send this request.

def test_get_all_fills(self):
‘’‘Get order_id from the response of the test_create_a_new_order() above.
Populate the rest of the query params if you need more filters.’‘’
params = {“order_id”: “729f1e45-74c6-4dfa-a244-10cf21cbb27a”, “before”: “”, “after”: “”}
auth = ExchangeRestAuth(self.key, self.secret, self.passphrase)
resp = requests.get(self.api_url + ‘/fills’, params=params, auth=auth)
resp_json = json.loads(resp.text)

1 Like

Thanks for your response Judy. I’m sending order_id as a query param but still getting {“message”:“invalid signature”}

import time, requests
import base64, hmac, hashlib
from creds import passphrase, api_key, secret

current_time = str(int(time.time()))

def get_fills():
    method = "GET"
    path = "/fills"
    order_id = "xxx"

    #create the prehash string by concatenating required parts
    message = "{}{}{}".format(current_time, method, path)
    # decode the base64 secret
    key = base64.b64decode(secret)
    # base64 encode the message
    msg = message.encode('utf-8')
    # create a sha256 hmac with the secret and sign the require message with the hmac
    hmac_hash = hmac.digest(key, msg, hashlib.sha256)
    # base64 encode the result
    sign = base64.b64encode(hmac_hash).decode('utf-8')

    headers = {
        "Accept": "application/json",
        'Content-Type': 'application/json',
        'CB-ACCESS-TIMESTAMP': current_time,
        'CB-ACCESS-KEY': api_key,
        'CB-ACCESS-PASSPHRASE': passphrase,
        'CB-ACCESS-SIGN': sign
        }
    url = "https://api.exchange.coinbase.com/fills?order_id="+order_id+"&profile_id=default&limit=100"
    response = requests.get(url, headers=headers)

Hi @Mike :wave: Welcome to the forum!

Can you try to print the response.url and share the response to us afterwards?

print(response.url)

#sample output:

https://api.exchange.coinbase.com/fills?order_id=729f1e45-74c6-4dfa-a244-10cf21cbb27a&before=&after=

Hi Camille, thanks for helping me out. When I print the response url:

print(response.url)

https://api.exchange.coinbase.com/fills?order_id=4a2fbcf8-9a64-4da1-9881-04797200cf89&profile_id=default&limit=100

I’m having trouble passing params. When I get request the “/accounts” endpoint it returns all my accounts successfully, but when I get request the “accounts/account_id” endpoint it 401’s with invalid signature.

import time, json, requests, base64, hmac, hashlib
from creds import passphrase, api_key, secret

def authentication(method, path):
    current_time = str(int(time.time()))
    message = "{}{}{}".format(current_time, method, path)
    key = base64.b64decode(secret)
    msg = message.encode('utf-8')
    hmac_hash = hmac.digest(key, msg, hashlib.sha256)
    sign = base64.b64encode(hmac_hash).decode('utf-8')
    headers = {
        "Accept": "application/json",
        'Content-Type': 'application/json',
        'CB-ACCESS-TIMESTAMP': current_time,
        'CB-ACCESS-KEY': api_key,
        'CB-ACCESS-PASSPHRASE': passphrase,
        'CB-ACCESS-SIGN': sign
        }
    return headers  

def get_accounts():
    method = "GET"
    path = "/accounts"
    headers = authentication(method, path)
    url = 'https://api.exchange.coinbase.com/accounts'
    response = requests.get(url, headers=headers)

    #print(response.text)
    print("response code:", response.status_code)
    print(response.url)

def get_single_account():
    method = "GET"
    path = "/accounts"
    params = { "account_id": "75411daf-867d-4b80-abd6-f7fe8e34c82e" }
    headers = authentication(method, path)
    url = "https://api.exchange.coinbase.com/accounts"
    response = requests.get(url, params=params, headers=headers)

    print(response.text)
    print("response code:", response.status_code)
    print(response.url)

get_accounts()
get_single_account()

Hi @Mike! In the code you provided, you are setting the path as the following:

def get_fills():
   method = "GET"
   path = "/fills"
   order_id = "xxx"

However the path should = /fills?order_id=729f1e45-74c6-4dfa-a244-10cf21cbb27a (order_id indicated is only a sample) and all other query params being passed. Here, path is equal to everything in the URL, excluding the https://api.exchange.coinbase.com

We hope this helps. Thank you for your patience!