'Sign error for bybit using c# when sending post request

I am trying to send a simple post request to the bybit api and I keep getting the 10004 sign error. Here is the response:

{"ret_code":10004,"ret_msg":"error sign! origin_string[api_key=(my api key)\u0026symbol=BTCUSDT\u0026timestamp=1635967650768]","ext_code":"","ext_info":"","result":null,"time_now":"1635967651.397800"}

This is the code I am using to send the request.

public async static Task<string> cancelAllOrders()
    {
        string ts = await GenerateTimeStamp();
        string paramString = "api_key=" + apiKey + "&symbol=BTCUSDT" + "timestamp=" + ts;
        string sign = CreateSignature(secretKey, paramString);
        CancelOrderContent co = new CancelOrderContent(apiKey, "BTCUSDT", ts, sign);

        var client = new RestClient(ApiUrl + "/v2/private/order/cancelAll");
        var request = new RestRequest();
        request.AddJsonBody(co);
        var response = client.Post(request);
        Trace.WriteLine(response.StatusCode.ToString() + "   " + response);
        return "";
    }

Here is the class I am Serializing to JSON for the body of the request.

public class CancelOrderContent
    {
        public string api_key;
        public string sign;
        public string symbol;
        public string timestamp;

        public CancelOrderContent(string api_key, string symbol, string timestamp,string sign)
        {
            this.api_key = api_key;
            this.symbol = symbol;
            this.timestamp = timestamp;
            this.sign = sign;
        }
    }

Here is the code I am using to create signatures:

public static string CreateSignature(string secret, string message)
        {
            var signatureBytes = Hmacsha256(Encoding.UTF8.GetBytes(secret), Encoding.UTF8.GetBytes(message));

            return ByteArrayToString(signatureBytes);
        }

        private static byte[] Hmacsha256(byte[] keyByte, byte[] messageBytes)
        {
            using (var hash = new HMACSHA256(keyByte))
            {
                return hash.ComputeHash(messageBytes);
            }
        }

I've tried al kinds of methods to fix this but I can't seem to get rid of it.I've tried mutliple endpoints and I still get the same error.



Solution 1:[1]

2022/01/17 this still works.

Hey @Vexatious I ran into a similar issue with the bybit-api trying to submit orders and I kept receiving a Key denied, insufficient permissions error even though I knew I was setting up my keys properly.

Maybe ByBit changed something? Lib outdated? Who Knows.

One major thing that I noticed is that they require you to Order the params alphabetically before appending the signature to the body of the request.

Edit: Updated because I recognized GET Requests can be equally as confusing. Scroll down to view the example POST Request.


GET and POST requests are handled differently

  1. axios(https://api-testnet.bybit.com/GET?aParam=foo&bParam=bar&sign=sign)
  2. axios(https://api-testnet.bybit.com/POST, {data: queryString})

SIGNATURE

For Both: You must alphabetically arrange the params before generating the signature.

  1. get your query param's on a single object [Including 'api_key', Excluding the API Secret].
  2. Sort the query param's alphabetically.
  3. Iterate over the objects sorted keys, building a QueryString like in the ex below
  4. use hmac sha256 with a hex digest to create the sign (look at ./sign.ts at the bottom)

ex: "aParam=foo&bParam=bar",

That's your sign parameter dealt with.


GET REQUESTS: Append the sign parameter to the end of the QueryString and send'er, might need to use a header

// Might still need {'Content-Type': 'application/x-www-form-urlencoded'}
// header depending on what request lib you're using.

const url = "https://api-testnet.bybit.com/GET?aParam=foo&bParam=bar&sign=" + sign

POST REQUESTS: it is required that the object is sent as request data (still, in the form of a QueryString like above) and not a fully built out Http string similar to the GET Request. Add the sign parameter to the end of the QueryString you generated the signature with, assign that to your request handlers data parameter and fire away!

I did come up with a minimal working version that successfully posted a Spot Order on their testnet, here is the jest test.

./bybit.test.ts

test("WORKING BYBIT TRADE.", async () => {
        const serverTime:number = (await axios.get(`https://api-testnet.bybit.com/spot/v1/time`)).data

        // These need to be within 1000ms of eachother (I'm pree sure, their formula is kinda confusing)
        console.log(`Their Timestamp`, serverTime)
        console.log(`Our Timestamp`, Date.now())

        const queryParams = {
            // Alphabetically ordered
            // (Sign generation function should deal with unordered params using .sort())
            'api_key': bybitApiKey,
            qty:10,
            recvWindow: 10000,
            side:"BUY",
            symbol:"ETHUSDT",
            timestamp: Date.now(),
            type:"MARKET",
        }

        const queryString = querystring.stringify(queryParams)
        const sign = "&sign=" + getSignature(queryParams, bybitSecret)
        const fullQuery = queryString + sign

        console.log(`FullQuery example`, fullQuery) 
        // api_key=...&qty=10&recvWindow=10000&side=BUY&symbol=ETHUSDT&timestamp=1638371037593&type=MARKET&sign=...

        let result = await axios(`https://api-testnet.bybit.com/spot/v1/order`, {
            withCredentials: true,
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            method: "POST",
            data: fullQuery,
        })

        console.log(`Post Status`, result.status)
        console.log(`Post Body`, result.data)
    })

    /**
        Post Status 200
        
        Post Body {
          ret_code: 0,
          ret_msg: '',
          ext_code: null,
          ext_info: null,
          result: {
            accountId: '...',
            symbol: 'ETHUSDT',
            symbolName: 'ETHUSDT',
            orderLinkId: '...',
            orderId: '...',
            transactTime: '...',
            price: '0',
            origQty: '10',
            executedQty: '0',
            status: 'FILLED',
            timeInForce: 'GTC',
            type: 'MARKET',
            side: 'BUY'
          }
    */
    }

./sign.ts

import crypto from 'crypto'

export function getSignature(parameters: any, secret: string) {
    var orderedParams = "";

    Object.keys(parameters).sort().forEach(function(key) {
        orderedParams += key + "=" + parameters[key] + "&";
    });
    orderedParams = orderedParams.substring(0, orderedParams.length - 1);

    return crypto.createHmac('sha256', secret).update(orderedParams).digest('hex');
}

Hopefully this helps!

Solution 2:[2]

I have just figured this out for the ByBit api, but using Javascript instead of C#. However i do know C#, so hopefully this will work for you.

The ByBit API endpoints which use POST require the same HMAC encryption of the data as in GET request. Instead of signing the parameters in the query string (and appending &sign=xxxx to it), you:

  • sign the serialized to JSON object,
  • then add the [object].sign = "xxxx" to the object before the POST.

This is where it gets tricky for your C# class. You have CancelOrderContent with a sign property. This will serialize the key 'sign' with the blank value. However, the ByBit API won't accept that because the data signed will be different.

Either you must

  • serialize an object without the 'sign' key (CancelOrderContentNosignkey),
  • copy the properties from CancelOrderContentNosignkey to a new CancelOrderContent
  • add the signature hash string to the CancelOrderContent object.sign prop
  • then post the serialized object with the sign key/value pair,

or...

  • serialize the object as you are now,
  • but then munge the serialized string to remove the ,"sign":'' part.
  • Then sign that string,
  • then add the sign value to the object,
  • serialise it to JSON again, and
  • POST that as the data.

I believe this will work, since i had to to that to get it working in JS. However, adding the sign key/value is easier since there's no classes.

A variation of this would be to make CancelOrderContent a dynamic type, where you don't add the 'sign' key/value until after serializing/signing it.

Note, when you manually serialize the object to JSON (do not use paramString), in theory the serializer should be the same one used or configured in RestRequest.AddJsonBody()

Sorry i don't have C# code, but this should work.

Solution 3:[3]

Thank you @Kwuasimoto for the detailed answer, it steered me in the right direction, however, it didn't quite work for me as is. When passing the fullQuery string as the axios data I was getting the error "missing required parameter 'sign'", but when I replaced the string with URLSearchParams like this

const postData = new URLSearchParams(queryParams);
postData.append("sign", signature);

it worked. Here's the full code I ended up using. It's mostly @Kwuasimoto 's answer with a few of my tweaks.

import crypto from 'crypto'
import axios from 'axios';

const getSignature(parameters: any, secret: string) => {
  let orderedParams = "";

  Object.keys(parameters).sort().forEach(function(key) {
    orderedParams += key + "=" + parameters[key] + "&";
  });
  orderedParams = orderedParams.substring(0, orderedParams.length - 1);

  return crypto.createHmac('sha256', secret).update(orderedParams).digest('hex');
}

const postSpotOrder = async () => {
  const queryParams = {
  api_key: bybitApiKey,
  qty: 10,
  recvWindow: 10000,
  side: "BUY",
  symbol: "ETHUSDT",
  timestamp: Date.now(),
  type: "MARKET",
};

  const signature = getSignature(queryParams, bybitSecret);
  const postData = new URLSearchParams(queryParams);
  postData.append("sign", signature);

  let result = await axios(`https://api-testnet.bybit.com/spot/v1/order`, {
    withCredentials: true,
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
    method: "POST",
    data: postData,
  });

  console.log(`Post Status`, result.status);
  console.log(`Post Body`, result.data);
};

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 David