Adv Trade API - Only getting Unauthorized Responses

Hey all, working through my connection endpoint right now, and consistently running into a 401 error. I anticipate the problem is with either the signature or the format of some aspect of the request, but can’t for the life of me figure it out. Support has been pointing me in the direction of corrections to my code, but I’m stalled at this point. My code is here: sampleCoinbaseConnection/coinbase.js at main · storshun/sampleCoinbaseConnection · GitHub
I’ve tried both /orders and /accounts endpoints with no luck. I would really appreciate any pointers on what I’ve done wrong. :pray:

Hi storshun, welcome to the forum! We understand that you are getting unauthorized responses when calling /orders and /accounts endpoints. Upon checking your code, we see here that your timestamp should be formatted to timestamp = Math.floor(Date.now() / 1000).toString(); .

Please note that based on the documentation, CB-ACCESS-TIMESTAMP MUST be a UNIX timestamp and it must be the same timestamp used to create the request signature. We would appreciate it if you rebuild your code by following the guidelines in the documentation for authenticating messages.

We hope this helps!

Hello Anonymouse, I appreciate the response, however that doesn’t change the outcome sadly. The timestamp I was getting from the coinbase server was a unix timestamp formatted in a string and used directly in the CB-ACCESS-TIMESTAMP header. I also tried modifying the timestamp to be generated by the code you suggested instead, however it didn’t change I’m receiving unauthorized as the response.

  // let timestamp = await getCurrentTime();
  timestamp = Math.floor(Date.now() / 1000).toString();

I seem to have found the source of the issue:

{
  method: 'GET',
  url: 'https://api.coinbase.com/api/v3/brokerage/accounts',
  headers: {
    'CB-ACCESS-KEY': 'r-redacted-0',
    'CB-ACCESS-SIGN': 'e4924ec46769145502d0a1f2a78d82237e20366a97018335e72b99e814352e39',
    'CB-ACCESS-TIMESTAMP': '1680360480',
    'Content-Type': 'application/json'
  },
  data: {}
}

And here’s the value of the headers from the server response, showing no mutation is happening:

_header: 'GET /api/v3/brokerage/accounts HTTP/1.1\r\n' +
        'Accept: application/json, text/plain, */*\r\n' +
        'CB-ACCESS-KEY: r-redacted-0\r\n' +
        'CB-ACCESS-SIGN: cedc8ea2dd22f804f5d30a9829f5cbea5c72b127908d97e8469e31b42e6ef2a6\r\n' +
        'CB-ACCESS-TIMESTAMP: 1680360975\r\n' +
        'Content-Type: application/json\r\n' +
        'User-Agent: axios/1.3.4\r\n' +
        'Content-Length: 2\r\n' +
        'Accept-Encoding: gzip, compress, deflate, br\r\n' +
        'Host: api.coinbase.com\r\n' +
        'Connection: close\r\n' +
        '\r\n',

As you can see, the signature is different, presumably because the timestamp coming back is also different.

I’ve logged several spots in the code to verify I’m not mutating it.

inside buildConfig: 1680361381
After signature creation: 1680361381```

Any ideas?
EDIT: I can’t repeat the behavior above, so I’m back to square one. Not sure what’s causing it.

I’ve also tried connecting via Postman using an extremely simple script, but am still getting unauthorized responses (401)

pm.globals.set("timestamp", Math.floor(Date.now()/1000).toString());
const secretKey = "s--------------------------h";
const requestPath = "/api/v3/brokerage/accounts";
const method = "GET"
const message = pm.globals.get("timestamp") + method + requestPath + "";
pm.globals.set("signature", CryptoJS.HmacSHA256(message, secretKey).toString(CryptoJS.enc.Hex));

I’m passing all the headers correctly, as shown here:

I’m really just out of ideas at this point.

A post was merged into an existing topic: Invalid product_id using /api/v3/brokerage/orders API

Couple potential problems here. Looks like you’re not JSON stringifying your data before you sign it. Axios will do this for you before it sends the request, so just make a JSON version right before you build the message, but don’t send that version with Axios or it will probably stringify it twice, garbling your request. Something like this should work I think:

  //Generate Signature
  const jsonData = config.data ? JSON.stringify(config.data) : "";
  const what = timestamp + config.method + requestPath + jsonData;
  config.headers["CB-ACCESS-SIGN"] = sign(what, secretKey);

The other problem I see is when you don’t have data in the request, you put an empty object.

data: {},

{} will stringify to {}, which you don’t want because again, you aren’t sending data with the request so it will mess up your signature. Try just using null instead.

data: null,

JSON.stringify would string that, but the ternary operator will see that as falsey, and add an empty string instead, which won’t affect your signature.

I think I got that all straight. Let me know if it works or what errors you get if it doesn’t.

1 Like

I was so hopeful. What you said makes sense, but unfortunately, I’m still getting 401s. Here’s my updated headers reflecting your suggested changes:

headers: AxiosHeaders {
    Accept: 'application/json, text/plain, */*',
    'CB-ACCESS-KEY': 'K-----------S',
    'CB-ACCESS-SIGN': 'f----all the stuff here-----6',
    'CB-ACCESS-TIMESTAMP': '1680660102',
    'Content-Type': 'application/json',
    'User-Agent': 'axios/1.3.4',
    'Content-Length': '4',
    'Accept-Encoding': 'gzip, compress, deflate, br'
  },
  method: 'get',
  url: 'https://api.coinbase.com/api/v3/brokerage/accounts',
  data: 'null'
}

Was rushing to type the last message and sent an incomplete message.

  let jsonData =  config.data ? JSON.stringify(config.data) : "";
  const what = cbTimeStamp + config.method + requestPath + jsonData;
  console.log("After signature creation: " + cbTimeStamp);
  config.headers["CB-ACCESS-SIGN"] = sign(what, secretKey);

and building the config info:

function buildConfig(apiKey, message, timestamp) {
  const { action } = message;
  let config = {
    method: "",
    url: "",
    headers: {
      "CB-ACCESS-KEY": apiKey,
      "CB-ACCESS-SIGN": "",
      "CB-ACCESS-TIMESTAMP": timestamp,
      "Content-Type": "application/json",
    },
    data: null,
  };

  //Determine Endpoint on Coinbase
  if (action === "getAccountData") {
    config.method = "GET";
    config.url = "/accounts";
  } else if (action === "getOrderList") {
    config.method = "GET";
    config.url = "/orders";
    config.data = { accountid: message.accountid };
  } else if (action === "cancelOrder") {
    //Needs Implementation
  } else {
    config.method = "POST";
    config.url = "/orders";
    config.data = {
      client_order_id: uuidv4(),
      product_id: message.symbol,
      side: action.toUpperCase(),
      order_configuration: {
        limit_limit_gtc: {
          base_size: message.quantity,
          limit_price: message.positionPrice,
          post_only: true,
        },
      },
    };
  }
  return config;
}

But alas, no love.

Okay, I got it. It’s in the headers. You have "Content-Type": "application/json",, change it to accept: "application/json", and then it should work if you send null as your data instead of "".

Here’s the entire js file with the working code, minus api stuff:

const axios = require("axios");
const CryptoJS = require("crypto-js");
const { v4: uuidv4 } = require("uuid");


const message = {
  action: "getAccountData",
  accountid: "accountid",
  symbol: "BTC-USD",
  quantity: "0.001",
  positionPrice: "10000",
};

const apiKey = "xxxxxxxxxx"

const secretKey = "xxxxxxxxxxxxxxxxxxxx"

const result = async () => {
  const result = await handleCoinbaseOrder(apiKey, secretKey, message);
  console.log(result, "result")
}

result();

async function handleCoinbaseOrder(apiKey, secretKey, message) {
  let timestamp = 1;

  timestamp = Math.floor(Date.now() / 1000).toString()


  //build the config object for the axios fetch
  let config = buildConfig(apiKey, message, timestamp);
  // let requestPath = "/api/v3/brokerage" + config.url;
  let requestPath = "/api/v3/brokerage" + config.url;
  config.url = "https://coinbase.com" + requestPath;

  //Generate Signature
  // const what = timestamp + config.method + requestPath + config.data;
  // config.headers["CB-ACCESS-SIGN"] = sign(what, secretKey);

  let jsonData = config.data != null ? JSON.stringify(config.data) : '';
  const what = timestamp + config.method + requestPath + jsonData;
  console.log("After signature creation: " + timestamp);
  config.headers["CB-ACCESS-SIGN"] = sign(what, secretKey);

  //Use API Authentication to access account via axios.
  try {
    console.log("config: ", config);
    const result = await axios(config);
    console.log("RESPONSE IS: ", result);
    return {
      result: result.data,
      // status: result.response.status,
      // statusText: result.response.statusText,
    };
  } catch (err) {
    console.log("Error response:\n", err);
    return { status: err.response?.status, statusText: err.response?.statusText };
  }
}

async function getCurrentTime() {
  let timestamp = await axios({
    method: "GET",
    url: "https://api.exchange.coinbase.com/time",
  });
  timestamp = Math.floor(timestamp.data.epoch).toString();
  console.log("timestamp: " + timestamp);
  return timestamp;
}

function buildConfig(apiKey, message, timestamp) {
  const { action } = message;
  let config = {
    method: "",
    url: "",
    headers: {
      "CB-ACCESS-KEY": apiKey,
      "CB-ACCESS-SIGN": "",
      "CB-ACCESS-TIMESTAMP": timestamp,
      accept: "application/json",
    },
    data: null,
  };

  //Determine Endpoint on Coinbase
  if (action === "getAccountData") {
    config.method = "GET";
    config.url = "/accounts";
    config.data = null;
  } else if (action === "getOrderList") {
    config.method = "GET";
    config.url = "/orders";
    config.data = { accountid: message.accountid };
  } else if (action === "cancelOrder") {
    //Needs Implementation
  } else {
    config.method = "POST";
    config.url = "/orders";
    config.data = {
      client_order_id: uuidv4(),
      product_id: message.symbol,
      side: action.toUpperCase(),
      order_configuration: {
        limit_limit_gtc: {
          base_size: message.quantity,
          limit_price: message.positionPrice,
          post_only: true,
        },
      },
    };
  }
  return config;
}

function sign(str, secret) {
  console.log("str: ", str);
  const hash = CryptoJS.HmacSHA256(str, secret).toString();
  return hash;
}
module.exports = handleCoinbaseOrder;


// {
//   method: 'GET',
//   url: 'https://api.coinbase.com/api/v3/brokerage/accounts',
//   headers: {
//     'CB-ACCESS-KEY': 'keyStringHere',
//     'CB-ACCESS-SIGN': 'b79359cc68c411b42c906c0b54c48192c8081b8c7c943609417c2a00dfd94a05',
//     'CB-ACCESS-TIMESTAMP': '1680303764',
//     'Content-Type': 'application/json'
//   },
//   data: {}
// }

I only tested the accounts endpoint, but it should at least solve the auth issue. Hope that helps!

1 Like

Well, this has all been very frustrating. The solution to the problem?
Stop presetting the value of data to empty or null. :person_facepalming:
jmicko, thank you for putting me onto that. As you pointed out, in the construction of the ‘config’ value, I had
data: {}
well, after changing it to
data: null
I was still getting unauthorized. So I just deleted the line all together and figured the various routes would insert it if necessary.
That worked.
I’m now getting correct messages back like “insufficient funds” or “base type too small”. Yay!
Off to build more functionality now :smiley:

Also, to be clear, jmicko’s part of the solution about JSON.stringify-ing the data before signing was part of the solution.

1 Like

Ah yeah, I did notice that this morning, but I wasn’t sure if it would still work on endpoints where you actually have data to send, and didn’t have enough time to test it. Glad to see you got it working!

1 Like