Node JS - POST request returns statusCode: 401 {"message":"invalid signature"}

as the title says…

I got the example posted by @larry.kubin for the GET request, which looks like this:

var crypto = require('crypto');
const https = require('https');

// current unix timestamp in seconds (note Date.now() returns milliseconds)
var cb_access_timestamp = Date.now() / 1000;

// obtained API key, secret, and passphrase from https://pro.coinbase.com/profile/api
var cb_access_passphrase = '';
var cb_api_key = '';
var secret = '';
var path = '/orders?status=done'; // fetching my completed orders
var body = ''; // body is empty in this case
var method = 'GET';

// create the prehash string by concatenating required parts
var message = cb_access_timestamp + method + path + body;

// decode the base64 secret
// NOTE: added .from() to address DeprecationWarning: Buffer() is deprecated
var key = Buffer.from(secret, 'base64');

// create a sha256 hmac with the secret
var hmac = crypto.createHmac('sha256', key);

// sign the require message with the hmac, and finally base64 encode the result
var cb_access_sign = hmac.update(message).digest('base64');

const options = {
  hostname: 'api.exchange.coinbase.com',
  port: 443,
  path: path,
  method: method,
  headers: {
    'Content-Type': 'application/json',
    'Content-Length': 0,
    'User-Agent': 'my-app',
    'CB-ACCESS-KEY': cb_api_key,
    'CB-ACCESS-PASSPHRASE': cb_access_passphrase,
    'CB-ACCESS-SIGN': cb_access_sign,
    'CB-ACCESS-TIMESTAMP': cb_access_timestamp
  }
}

const req = https.request(options, res => {
  console.log(`statusCode: ${res.statusCode}`)

  res.on('data', d => {
    process.stdout.write(d)
  })
})

req.on('error', error => {
  console.error(error)
})

req.end()

I got my api key, passphrase and secret stored in a .env file
Everything works great. If I do a manual transaction I can use this script and query the filled orders.
Returns status 200
And all the details about the transaction.

Furthermore, I wanted to create a similar request for posting a transaction.
Changed the method to POST, built a body{} this time and used the same methods as in the GET request.
However, I am getting:

statusCode: 401
{"message":"invalid signature"}

If I leave the body empty like in the GET request, I am getting:

statusCode: 400
{"message":"product_id is not a valid product"}

which tells me that the request is going through, but it is expecting parameters for the body.

Below is my entire code that I have tried to put together for this task. I have used both the crypto package recommended in the api docs, as well as the CryptoJS package suggested by @shurr .
Both scenarios return the same status code:

statusCode: 401
{"message":"invalid signature"}

Here’s my full code:

const config = require('dotenv').config();
var crypto = require('crypto');
//var CryptoJS = require('crypto-js');
const https = require('https');

// current unix timestamp in seconds (note Date.now() returns milliseconds)
var cb_access_timestamp = Date.now() / 1000;

// obtained API key, secret, and passphrase from https://pro.coinbase.com/profile/api
var cb_access_passphrase = process.env.cb_access_passphrase;
var cb_api_key = process.env.cb_api_key;
var cb_secret = process.env.cb_secret;
var path = '/orders';
//var path = '/trade';
//var body = '';
var body = JSON.stringify({
    "product_id":"BTC-USD",
    "side":"buy",
    "type":"limit",
    "size":"0.0001",
    "price":"44415.51"
});

var method = 'POST';


// create the prehash string by concatenating required parts
var message = cb_access_timestamp + method + path + body;

var key = Buffer.from(cb_secret, 'base64');
var hmac = crypto.createHmac('sha256', key);
var sign = hmac.update(message).digest('base64');

//var key = CryptoJS.enc.Base64.parse(cb_secret);
//var hash = CryptoJS.HmacSHA256(message, key);
//var sign = hash.toString(CryptoJS.enc.Base64);

const options = {
  hostname: 'api.exchange.coinbase.com',
  //hostname: 'api.pro.coinbase.com',
  port: 443,
  path: path,
  method: method,
  headers: {
    'Content-Type': 'application/json',
    'Content-Length': 0,
    'User-Agent': 'MyTradingBot',
    'CB-ACCESS-KEY': cb_api_key,
    'CB-ACCESS-PASSPHRASE': cb_access_passphrase,
    'CB-ACCESS-SIGN': sign,
    'CB-ACCESS-TIMESTAMP': cb_access_timestamp
  }
}

const req = https.request(options, res => {
    console.log(message);
    console.log(`statusCode: ${res.statusCode}`)
    
    res.on('data', d => {
        process.stdout.write(d)
    })
})

req.on('error', error => {
  console.error(error)
})

req.end()

Apologies for the lengthy post, but I was trying to describe my issue as detailed as possible.
Any help is greatly appreciated.

Awwww, you know that feeling as a kid when you wake up in the morning and rush downstairs to play with your newest toy, only to find out that it’s broken and doesn’t work anymore?
Lol.
I was hoping one of the devs see my cries for help and would be willing to point me in the right direction…

Laaaaaaaryyyyy? Where art though? @larry.kubin :slight_smile:
All joking aside, I know the devs are super busy, but the api docs have bigger holes than the one at the center of the Milky Way.
:sob:

There’s enough working code in this subforum where you can figure things out, find out where you’re going wrong.

I’m not a very good coder, but even I was eventually able to place orders 3 ways: python, AHK, and a combo of CryptoJS and AHK.

Well, I’m sorry but I can’t find this vast info you speak of.
Maybe I’m not as good of a coder as you… still, that’s the reason I’m here to learn more.
There’s a total of 34 threads in this sub forum, with only 3 pertaining to the issues with the signature. None of them specific for Node Js. And yes I have read all 3 of them.
I’m trying… believe me, I’d like to be able to play with this a bit, but the information is poor, the api docs are a mess and the only working code I could find was the one Larry posted initially for the GET request. That is what I’m basing my script on and trying to adjust for a POST.

As I said, I may not be a good enough coder… but you can’t just tell me… oh the info must be out there somewhere. Cause, so far I haven’t found anything.

@cleggink mentioned that I have my keys in the wrong place. I tried switching that up and am getting a:

statusCode: 401
{"message":"Invalid API Key"}

as opposed to my previous:

statusCode: 401
{"message":"invalid signature"}

maybe I defined them wrong from the beginning?
This is how I have them set up:

cb_access_passphrase = xxxxxxxxxx //10 characters long
cb_api_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx //32 chars long
cb_secret = xxxxx..........xxxxxxxxxxxxxxxxxxxxxxxxxxxxx //88 chars long

did I mix these up?

As you figured out, the API key must be part of the header, along with the other elements.

The signature is made from the API secret and the concatenated message. Your message looked well-formed to me. Your secret’s probably correct. So the issue might be how the signature is made.

Is there a nodeJS forum where you can get help? I got help at an AHK forum.

I’m sure there might be a node forum that can check my syntax and logic… but only to an extent, where someone actually used this api. since the responses are being kind of generic.
I’ll see what I can find. But was hoping that coming to the source would be the best way.
Don’t get me wrong @shurr , I really appreciate your help and trying to figure this out. You seem like a nice guy and are active, which is great.

Ok, one thing that I have noticed is that my account does not have permissions for the Exchange website:
Coinbase Exchange Website
So not sure if my credentials would even work with the api.exchange.coinbase.com
(Since that is built for institutional investors).
But it should work on api.pro.coinbase.com where I actually do all my manual trades and where I actually set up my API key.
Again, I am talking about the production api not sandbox. Which also means that the paths and options might be different.
A little shedding of light would be greatly appreciated from the devs or mods.
E.g. on Pro… I place my trades on ‘/trade/BTC_USD’ not sure how this is set up on the Exchange.
On Pro… ‘/orders’ only shows open, filled and fees. There’s no option to place any trades there.
I’m talking about the website, the paths in the api’s might be the same. No clue.
Just a little more digging and ranting :slight_smile:

Did you try adding body to options:
const options = {
body: body

yup. this is what I have now:

const options = {
  //hostname: 'api.exchange.coinbase.com',
  hostname: 'api.pro.coinbase.com',
  port: 443,
  path: path,
  method: method,
  headers: {
    'Content-Type': 'application/json',
    //'Content-Length': 0,
    'User-Agent': 'MyTradingBot',
    'CB-ACCESS-KEY': cb_api_key,
    'CB-ACCESS-PASSPHRASE': cb_access_passphrase,
    'CB-ACCESS-SIGN': sign,
    'CB-ACCESS-TIMESTAMP': cb_access_timestamp
  },
  body: body
}

Then if everything else is fine, the functions making the signature are the issue. What you need to do is make a signature with a working code, plugging in a short base64 string and a short message string. Then match the signature output by using nodeJS functions. Something like the following.

cbSecret = ‘6e+uT/5==’
cbMsg = ‘q’

Plugging the above into python functions generates:

4ce7iOHt7Cg6ywurrFMR6USovPvzN/QS+Eu83OCVLhY=

I promise you I will get to the bottom of this… I have found a working code which I will post shortly.
But the signature is generated identically.

A little later than expected, but as I promised, here is a full working script.
This works on the production server. Tried, tested and confirmed.
Confirmed for Market Orders and Limit Orders.

The only major difference from what I have tried before is the way the connection to the api is established. My first example was using the https package as in @larry.kubin 's example. This one uses the request package. (which is fully deprecated).

var crypto = require('crypto');
var request = require('request');
//const https = require('https');
const config = require('dotenv').config();

async function cb_request( method, path, headers = {}, body = ''){

    var apiKey = process.env.cb_api_key,
        apiSecret = process.env.cb_secret,
        apiPass = process.env.cb_access_passphrase;

    //get unix time in seconds
    var timestamp = Math.floor(Date.now() / 1000);

    // set the request message
    var message = timestamp + method + path + body;
    console.log('######## message=' + message);

    //create a hexedecimal encoded SHA256 signature of the message
    var key = Buffer.from(apiSecret, 'base64');
    var signature = crypto.createHmac('sha256', key).update(message).digest('base64');

    //create the request options object
    var baseUrl = 'https://api.pro.coinbase.com';

    headers = Object.assign({},headers,{
        'content-type': 'application/json; charset=UTF-8', //missing
        'CB-ACCESS-SIGN': signature,
        'CB-ACCESS-TIMESTAMP': timestamp,
        'CB-ACCESS-KEY': apiKey,
        'CB-ACCESS-PASSPHRASE': apiPass,
        'USER-AGENT': 'request'
    });

    // Logging the headers here to ensure they're sent properly
    console.log(headers);

    var options = {
        'baseUrl': baseUrl,
        'url': path,
        'method': method,
        'headers': headers,
        'body': body //missing
    };

    return new Promise((resolve,reject)=>{
    request( options, function(err, response, body){
        console.log(response.statusCode + "  " + response.statusMessage);
        if (err) reject(err);
        resolve(JSON.parse(response.body));
        });
    });
}

async function main() {
    
    // This queries a product by id (successfully)
    try {
        console.log('try to call product------->');
        console.log( await cb_request('GET','/products/BTC-USD') );
        console.log('product------------------->done');
    }
    catch(e) {
        console.log(e);
    }

    /* var buyParams = JSON.stringify({ // Use this for a Market Order
    'type': 'market',
    'side': 'buy',
    'funds': '5',
    'product_id': 'BTC-USD'
    }); */
    var buyParams = JSON.stringify({ //Use this for a Limit Order
        'type': 'limit',
        'side': 'buy',
        'size': '0.0001',
        'price': '40151.51',
        'product_id': 'BTC-USD'
    });

    try {
    console.log('try to call orders------->');
    var buy = await cb_request('POST','/orders', {}, buyParams);
    console.log(buy);
    console.log('orders----------------------->done');

    }
    catch(e) {
        console.log(e);
    }

}

main();

@cnapsys What was making your sig invalid?

Hi @cnapsys, first of all, thank you very much for your enthusiasm in trying out the Coinbase APIs. We apologize for the delay in response, but we have already escalated this to a team of specialists which will review your case related to the invalid_signature error message.

We’ll update you as soon as we can. Thank you for your patience and understanding.

Hey @cnapsys, I programmed a great Coinbase API for Node.js.

You are welcome to look at my code to see how I solved the signing: https://github.com/bennycode/coinbase-pro-node/blob/v5.0.0/src/auth/RequestSigner.ts

2 Likes