Skip to content

代码示例

本文档提供不同编程语言的API接入示例,适用于所有需要签名的接口(现货和合约通用)。

签名生成方法

所有需要签名的接口(TRADEUSER_DATA)都需要使用 HMAC SHA256 算法生成签名。

Python

python
import hmac
import hashlib
import time

def generate_signature(secret_key, params):
    """
    生成HMAC SHA256签名
    :param secret_key: API Secret Key
    :param params: 参数字典(不包含signature)
    :return: 签名字符串
    """
    # 参数拼接成query string格式
    query_string = "&".join([f"{k}={v}" for k, v in params.items()])
    
    # 生成HMAC SHA256签名
    signature = hmac.new(
        secret_key.encode('utf-8'),
        query_string.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    return signature

Java

java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.stream.Collectors;

public class SignatureUtil {
    public static String generateSignature(String secretKey, String queryString) 
            throws NoSuchAlgorithmException, InvalidKeyException {
        // 生成HMAC SHA256签名
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKeySpec = new SecretKeySpec(
            secretKey.getBytes(StandardCharsets.UTF_8), 
            "HmacSHA256"
        );
        mac.init(secretKeySpec);
        
        byte[] hash = mac.doFinal(queryString.getBytes(StandardCharsets.UTF_8));
        return bytesToHex(hash);
    }
    
    private static String bytesToHex(byte[] bytes) {
        StringBuilder result = new StringBuilder();
        for (byte b : bytes) {
            result.append(String.format("%02x", b));
        }
        return result.toString();
    }
}

Node.js

javascript
const crypto = require('crypto');

/**
 * 生成HMAC SHA256签名
 * @param {string} secretKey - API Secret Key
 * @param {Object} params - 参数字典(不包含signature)
 * @returns {string} 签名字符串
 */
function generateSignature(secretKey, params) {
    // 参数拼接成query string格式
    const queryString = Object.keys(params)
        .map(key => `${key}=${params[key]}`)
        .join('&');
    
    // 生成HMAC SHA256签名
    const signature = crypto
        .createHmac('sha256', secretKey)
        .update(queryString)
        .digest('hex');
    
    return signature;
}

Go

go
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "sort"
    "strings"
)

// GenerateSignature 生成HMAC SHA256签名
// 注意:Go的map遍历顺序随机,需按参数名排序以保证签名确定性
func GenerateSignature(secretKey string, params map[string]string) string {
    // 按参数名排序后拼接query string
    keys := make([]string, 0, len(params))
    for k := range params {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    var parts []string
    for _, k := range keys {
        parts = append(parts, fmt.Sprintf("%s=%s", k, params[k]))
    }
    queryString := strings.Join(parts, "&")
    
    // 生成HMAC SHA256签名
    mac := hmac.New(sha256.New, []byte(secretKey))
    mac.Write([]byte(queryString))
    signature := hex.EncodeToString(mac.Sum(nil))
    
    return signature
}

完整请求示例

以下示例展示如何调用一个需要签名的接口(适用于所有 TRADEUSER_DATA 接口)。

Python完整示例

python
import requests
import hmac
import hashlib
import time

def call_signed_api(endpoint, params, api_key, secret_key, method='POST'):
    """
    调用需要签名的API接口
    :param endpoint: API端点路径,如 '/api/v1/futures/order' 或 '/api/v1/spot/order'
    :param params: 参数字典
    :param api_key: API Key
    :param secret_key: Secret Key
    :param method: HTTP方法,'GET' 或 'POST'
    :return: 响应结果
    """
    # 添加时间戳(必需)
    params['timestamp'] = int(time.time() * 1000)
    
    # 可选:添加recvWindow
    if 'recvWindow' not in params:
        params['recvWindow'] = 5000
    
    # 生成签名(使用相同的query_string格式)
    query_string_for_sign = "&".join([f"{k}={v}" for k, v in params.items()])
    signature = hmac.new(
        secret_key.encode('utf-8'),
        query_string_for_sign.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    params['signature'] = signature
    
    # 构建完整的query string(包含signature,与签名时使用相同的顺序)
    query_string = "&".join([f"{k}={v}" for k, v in params.items()])
    
    # 发送请求(使用相同的query_string格式,确保顺序一致)
    url = f"https://api.toobit.com{endpoint}"
    headers = {"X-BB-APIKEY": api_key}
    
    if method == 'GET':
        # GET请求:手动构建query string,确保与签名时顺序一致
        url_with_params = f"{url}?{query_string}"
        response = requests.get(url_with_params, headers=headers)
    else:
        # POST请求:使用手动构建的query string作为body,确保顺序一致
        headers["Content-Type"] = "application/x-www-form-urlencoded"
        response = requests.post(url, headers=headers, data=query_string)
    
    return response.json()

# 使用示例
if __name__ == "__main__":
    api_key = "your-api-key"
    secret_key = "your-secret-key"
    
    # 示例1:查询账户余额(GET请求)
    params = {
        "recvWindow": 5000
    }
    result = call_signed_api("/api/v1/futures/balance", params, api_key, secret_key, method='GET')
    print(result)
    
    # 示例2:下单(POST请求)
    order_params = {
        "symbol": "BTC-SWAP-USDT",
        "side": "BUY_OPEN",
        "type": "LIMIT",
        "quantity": "10",
        "price": "30000",
        "newClientOrderId": "test_order_001",
        "recvWindow": 5000
    }
    result = call_signed_api("/api/v1/futures/order", order_params, api_key, secret_key, method='POST')
    print(result)

Java完整示例

java
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

public class ApiClient {
    private String apiKey;
    private String secretKey;
    private String baseUrl = "https://api.toobit.com";
    
    public ApiClient(String apiKey, String secretKey) {
        this.apiKey = apiKey;
        this.secretKey = secretKey;
    }
    
    private String buildQueryString(Map<String, String> params) {
        // 构建query string(与签名和请求提交时使用相同的顺序)
        return params.entrySet().stream()
                .map(e -> e.getKey() + "=" + e.getValue())
                .collect(Collectors.joining("&"));
    }
    
    public String callSignedApi(String endpoint, Map<String, String> params, String method) 
            throws Exception {
        // 添加时间戳
        params.put("timestamp", String.valueOf(System.currentTimeMillis()));
        
        // 添加recvWindow(如果不存在)
        if (!params.containsKey("recvWindow")) {
            params.put("recvWindow", "5000");
        }
        
        // 生成签名(使用buildQueryString确保顺序一致)
        String queryStringForSign = buildQueryString(params);
        String signature = SignatureUtil.generateSignature(secretKey, queryStringForSign);
        params.put("signature", signature);
        
        // 构建query string(与签名时使用相同的顺序)
        String queryString = buildQueryString(params);
        
        // 构建URL
        String urlString = baseUrl + endpoint;
        if ("GET".equals(method)) {
            urlString += "?" + queryString;
        }
        
        // 发送请求
        URL url = new URL(urlString);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod(method);
        conn.setRequestProperty("X-BB-APIKEY", apiKey);
        
        if ("POST".equals(method)) {
            conn.setDoOutput(true);
            // POST请求:使用与签名时相同的query string作为body
            try (OutputStream os = conn.getOutputStream()) {
                os.write(queryString.getBytes(StandardCharsets.UTF_8));
            }
        }
        
        // 读取响应
        BufferedReader reader = new BufferedReader(
                new InputStreamReader(conn.getInputStream()));
        StringBuilder response = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            response.append(line);
        }
        reader.close();
        
        return response.toString();
    }
    
    // 使用示例
    public static void main(String[] args) throws Exception {
        ApiClient client = new ApiClient("your-api-key", "your-secret-key");
        
        // 示例1:查询账户余额(GET请求)
        Map<String, String> params1 = new LinkedHashMap<>();
        params1.put("recvWindow", "5000");
        String result1 = client.callSignedApi("/api/v1/futures/balance", params1, "GET");
        System.out.println(result1);
        
        // 示例2:下单(POST请求)
        Map<String, String> params2 = new LinkedHashMap<>();
        params2.put("symbol", "BTC-SWAP-USDT");
        params2.put("side", "BUY_OPEN");
        params2.put("type", "LIMIT");
        params2.put("quantity", "10");
        params2.put("price", "30000");
        params2.put("newClientOrderId", "test_order_001");
        params2.put("recvWindow", "5000");
        String result2 = client.callSignedApi("/api/v1/futures/order", params2, "POST");
        System.out.println(result2);
    }
}

Node.js完整示例

javascript
const crypto = require('crypto');
const https = require('https');
const querystring = require('querystring');

class ApiClient {
    constructor(apiKey, secretKey) {
        this.apiKey = apiKey;
        this.secretKey = secretKey;
        this.baseUrl = 'https://api.toobit.com';
    }
    
    buildQueryString(params) {
        // 构建query string(与签名和请求提交时使用相同的顺序)
        const keys = Object.keys(params);
        return keys
            .map(key => `${key}=${params[key]}`)
            .join('&');
    }
    
    generateSignature(params) {
        // 使用buildQueryString确保顺序一致
        const queryString = this.buildQueryString(params);
        
        return crypto
            .createHmac('sha256', this.secretKey)
            .update(queryString)
            .digest('hex');
    }
    
    async callSignedApi(endpoint, params, method = 'POST') {
        // 添加时间戳
        params.timestamp = Date.now();
        
        // 添加recvWindow(如果不存在)
        if (!params.recvWindow) {
            params.recvWindow = 5000;
        }
        
        // 生成签名
        const signature = this.generateSignature(params);
        params.signature = signature;
        
        // 构建query string(与签名时使用相同的顺序)
        const queryString = this.buildQueryString(params);
        
        // 构建请求
        const url = new URL(this.baseUrl + endpoint);
        const options = {
            method: method,
            headers: {
                'X-BB-APIKEY': this.apiKey
            }
        };
        
        if (method === 'GET') {
            // GET请求:使用与签名时相同的query string
            url.search = queryString;
        } else {
            // POST请求:使用与签名时相同的query string作为body
            options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
        }
        
        return new Promise((resolve, reject) => {
            const req = https.request(url, options, (res) => {
                let data = '';
                res.on('data', (chunk) => {
                    data += chunk;
                });
                res.on('end', () => {
                    try {
                        resolve(JSON.parse(data));
                    } catch (e) {
                        resolve(data);
                    }
                });
            });
            
            req.on('error', (e) => {
                reject(e);
            });
            
            if (method === 'POST') {
                // POST请求:使用与签名时相同的query string
                req.write(queryString);
            }
            
            req.end();
        });
    }
}

// 使用示例
async function main() {
    const client = new ApiClient('your-api-key', 'your-secret-key');
    
    // 示例1:查询账户余额(GET请求)
    try {
        const result1 = await client.callSignedApi(
            '/api/v1/futures/balance',
            { recvWindow: 5000 },
            'GET'
        );
        console.log(result1);
    } catch (error) {
        console.error('Error:', error);
    }
    
    // 示例2:下单(POST请求)
    try {
        const result2 = await client.callSignedApi(
            '/api/v1/futures/order',
            {
                symbol: 'BTC-SWAP-USDT',
                side: 'BUY_OPEN',
                type: 'LIMIT',
                quantity: '10',
                price: '30000',
                newClientOrderId: 'test_order_001',
                recvWindow: 5000
            },
            'POST'
        );
        console.log(result2);
    } catch (error) {
        console.error('Error:', error);
    }
}

main();

Go完整示例

go
package main

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "strconv"
    "strings"
    "time"
)

type ApiClient struct {
    apiKey    string
    secretKey string
    baseUrl   string
}

func NewApiClient(apiKey, secretKey string) *ApiClient {
    return &ApiClient{
        apiKey:    apiKey,
        secretKey: secretKey,
        baseUrl:   "https://api.toobit.com",
    }
}

func (c *ApiClient) buildQueryString(params map[string]string, keys []string) string {
    // 构建query string(使用传入的keys顺序,确保签名和请求一致)
    // 注意:Go的map遍历顺序是随机的,所以必须使用相同的keys slice来构建query string
    var parts []string
    for _, k := range keys {
        if v, exists := params[k]; exists {
            parts = append(parts, fmt.Sprintf("%s=%s", k, v))
        }
    }
    return strings.Join(parts, "&")
}

func (c *ApiClient) getParamKeys(params map[string]string) []string {
    // 获取参数的键列表(顺序固定,用于确保签名和请求一致)
    keys := make([]string, 0, len(params))
    for k := range params {
        keys = append(keys, k)
    }
    return keys
}

func (c *ApiClient) generateSignature(params map[string]string, keys []string) string {
    // 构建query string(使用传入的keys顺序,确保与请求提交时一致)
    queryString := c.buildQueryString(params, keys)
    
    // 生成HMAC SHA256签名
    mac := hmac.New(sha256.New, []byte(c.secretKey))
    mac.Write([]byte(queryString))
    signature := hex.EncodeToString(mac.Sum(nil))
    
    return signature
}

func (c *ApiClient) CallSignedApi(endpoint string, params map[string]string, method string) (map[string]interface{}, error) {
    // 添加时间戳
    params["timestamp"] = strconv.FormatInt(time.Now().UnixMilli(), 10)
    
    // 添加recvWindow(如果不存在)
    if _, exists := params["recvWindow"]; !exists {
        params["recvWindow"] = "5000"
    }
    
    // 获取参数的键列表(用于确保签名和请求使用相同的顺序)
    keys := c.getParamKeys(params)
    
    // 生成签名(使用keys顺序)
    signature := c.generateSignature(params, keys)
    params["signature"] = signature
    
    // 在原有keys后追加signature,保持顺序一致(Go的map遍历顺序随机,不能重新getParamKeys)
    keysForRequest := append(keys, "signature")
    
    // 构建query string(与签名时使用相同的keys顺序)
    queryString := c.buildQueryString(params, keysForRequest)
    
    var req *http.Request
    var err error
    
    url := c.baseUrl + endpoint
    
    if method == "GET" {
        // GET请求:使用与签名时相同的query string
        url += "?" + queryString
        req, err = http.NewRequest(method, url, nil)
    } else {
        // POST请求:使用与签名时相同的query string作为body
        req, err = http.NewRequest(method, url, strings.NewReader(queryString))
        req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    }
    
    if err != nil {
        return nil, err
    }
    
    req.Header.Set("X-BB-APIKEY", c.apiKey)
    
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    
    var result map[string]interface{}
    err = json.Unmarshal(body, &result)
    if err != nil {
        return nil, err
    }
    
    return result, nil
}

// 使用示例
func main() {
    client := NewApiClient("your-api-key", "your-secret-key")
    
    // 示例1:查询账户余额(GET请求)
    params1 := map[string]string{
        "recvWindow": "5000",
    }
    result1, err := client.CallSignedApi("/api/v1/futures/balance", params1, "GET")
    if err != nil {
        fmt.Printf("Error: %v\n", err)
    } else {
        fmt.Printf("Result: %+v\n", result1)
    }
    
    // 示例2:下单(POST请求)
    params2 := map[string]string{
        "symbol":          "BTC-SWAP-USDT",
        "side":            "BUY_OPEN",
        "type":            "LIMIT",
        "quantity":        "10",
        "price":           "30000",
        "newClientOrderId": "test_order_001",
        "recvWindow":      "5000",
    }
    result2, err := client.CallSignedApi("/api/v1/futures/order", params2, "POST")
    if err != nil {
        fmt.Printf("Error: %v\n", err)
    } else {
        fmt.Printf("Result: %+v\n", result2)
    }
}

cURL示例

bash
#!/bin/bash

API_KEY="your-api-key"
SECRET_KEY="your-secret-key"
BASE_URL="https://api.toobit.com"

# 生成签名函数
generate_signature() {
    local params="$1"
    echo -n "$params" | openssl dgst -sha256 -hmac "$SECRET_KEY" | awk '{print $2}'
}

# 调用需要签名的API
call_signed_api() {
    local endpoint="$1"
    local method="${2:-POST}"
    local params="$3"
    
    # 添加时间戳
    local timestamp=$(date +%s)000
    if [ -z "$params" ]; then
        params="timestamp=${timestamp}"
    else
        params="${params}&timestamp=${timestamp}"
    fi
    
    # 添加recvWindow(如果不存在)
    if [[ ! "$params" =~ recvWindow ]]; then
        params="${params}&recvWindow=5000"
    fi
    
    # 生成签名
    local signature=$(generate_signature "$params")
    params="${params}&signature=${signature}"
    
    # 发送请求
    if [ "$method" = "GET" ]; then
        curl -X GET "${BASE_URL}${endpoint}?${params}" \
            -H "X-BB-APIKEY: ${API_KEY}"
    else
        curl -X POST "${BASE_URL}${endpoint}" \
            -H "X-BB-APIKEY: ${API_KEY}" \
            -H "Content-Type: application/x-www-form-urlencoded" \
            -d "${params}"
    fi
}

# 使用示例
# 示例1:查询账户余额(GET请求)
call_signed_api "/api/v1/futures/balance" "GET" ""

# 示例2:下单(POST请求)
call_signed_api "/api/v1/futures/order" "POST" \
    "symbol=BTC-SWAP-USDT&side=BUY_OPEN&type=LIMIT&quantity=10&price=30000&newClientOrderId=test_001"

JSON 请求体验签示例(API v2)

/api/v2/Content-Type: application/json 的接口(如下单、批量下单)采用以下验签方式:

签名字符串拼接规则:

待签名字符串 = query string(不含 signature)+ 原始 JSON 请求体

两部分之间无任何分隔符直接拼接。

请求结构:

位置参数
URL 查询参数timestamprecvWindow(可选)、signature 及其他公共参数(如 category
请求头X-BB-APIKEYContent-Type: application/json
请求体原始 JSON 字符串(与参与签名的内容字符完全一致,必须为紧凑格式,不含换行;服务端按行读取 body 时会丢弃换行符,格式化 JSON 会导致验签失败)

Python

python
import hmac
import hashlib
import json
import time
import requests

API_KEY  = "your-api-key"
SECRET   = "your-secret-key"
BASE_URL = "https://api.toobit.com"

def v2_json_request(endpoint, body, extra_query=None):
    """
    V2 JSON body 签名请求。
    签名字符串 = query string(不含 signature)+ 原始 JSON body(无分隔符)
    timestamp / recvWindow / signature 放 URL 查询参数,业务参数放 JSON body。
    """
    timestamp = int(time.time() * 1000)
    query = {"timestamp": timestamp, "recvWindow": 5000}
    if extra_query:
        query.update(extra_query)

    # 序列化 JSON body,必须与实际发送的请求体完全一致
    json_body = json.dumps(body, separators=(',', ':'))

    # 签名字符串 = query string + JSON body(无分隔符)
    query_str = "&".join(f"{k}={v}" for k, v in query.items())
    sign_str  = query_str + json_body

    sig = hmac.new(SECRET.encode(), sign_str.encode(), hashlib.sha256).hexdigest()

    url     = f"{BASE_URL}{endpoint}?{query_str}&signature={sig}"
    headers = {"X-BB-APIKEY": API_KEY, "Content-Type": "application/json"}
    return requests.post(url, headers=headers, data=json_body).json()


# 示例 1:下普通单(JSON object body)
order = {
    "symbol": "BTC-SWAP-USDT",
    "side": "BUY",
    "positionSide": "LONG",
    "type": "LIMIT",
    "newClientOrderId": "v2_order_001",
    "quantity": "1",
    "price": "30000",
}
print(v2_json_request("/api/v2/futures/order", order))

# 示例 2:批量下单(JSON array body)
batch = [
    {"symbol": "BTC-SWAP-USDT", "side": "BUY", "positionSide": "LONG",
     "type": "LIMIT", "newClientOrderId": "batch_001", "quantity": "1", "price": "30000"},
    {"symbol": "ETH-SWAP-USDT", "side": "SELL", "positionSide": "SHORT",
     "type": "LIMIT", "newClientOrderId": "batch_002", "quantity": "2", "price": "2000"},
]
print(v2_json_request("/api/v2/futures/batch-orders", batch, extra_query={"category": "USDT"}))

Java

java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Instant;

public class V2JsonApiClient {

    private static final String API_KEY  = "your-api-key";
    private static final String SECRET   = "your-secret-key";
    private static final String BASE_URL = "https://api.toobit.com";

    static String hmacSha256(String key, String data) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
        StringBuilder sb = new StringBuilder();
        for (byte b : hash) sb.append(String.format("%02x", b));
        return sb.toString();
    }

    /**
     * V2 JSON body 签名请求。
     * 签名字符串 = query string(不含 signature)+ 原始 JSON body(无分隔符)。
     *
     * @param endpoint   接口路径,如 /api/v2/futures/order
     * @param jsonBody   原始 JSON 字符串(必须与实际发送的请求体完全一致)
     * @param extraQuery 额外查询参数,如 "category=USDT",不需要时传 null
     */
    static String v2JsonRequest(String endpoint, String jsonBody, String extraQuery) throws Exception {
        long timestamp = Instant.now().toEpochMilli();
        String queryStr = "timestamp=" + timestamp + "&recvWindow=5000"
                + (extraQuery != null && !extraQuery.isEmpty() ? "&" + extraQuery : "");

        // 签名字符串 = query string + JSON body(无分隔符)
        String signature = hmacSha256(SECRET, queryStr + jsonBody);

        String url = BASE_URL + endpoint + "?" + queryStr + "&signature=" + signature;
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .header("X-BB-APIKEY", API_KEY)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8))
                .build();

        return HttpClient.newHttpClient()
                .send(request, HttpResponse.BodyHandlers.ofString())
                .body();
    }

    public static void main(String[] args) throws Exception {
        // 示例 1:下普通单
        String orderBody = "{\"symbol\":\"BTC-SWAP-USDT\",\"side\":\"BUY\",\"positionSide\":\"LONG\","
                + "\"type\":\"LIMIT\",\"newClientOrderId\":\"v2_order_001\","
                + "\"quantity\":\"1\",\"price\":\"30000\"}";
        System.out.println(v2JsonRequest("/api/v2/futures/order", orderBody, null));

        // 示例 2:批量下单(body 为 JSON 数组)
        String batchBody = "[{\"symbol\":\"BTC-SWAP-USDT\",\"side\":\"BUY\",\"positionSide\":\"LONG\","
                + "\"type\":\"LIMIT\",\"newClientOrderId\":\"batch_001\",\"quantity\":\"1\",\"price\":\"30000\"},"
                + "{\"symbol\":\"ETH-SWAP-USDT\",\"side\":\"SELL\",\"positionSide\":\"SHORT\","
                + "\"type\":\"LIMIT\",\"newClientOrderId\":\"batch_002\",\"quantity\":\"2\",\"price\":\"2000\"}]";
        System.out.println(v2JsonRequest("/api/v2/futures/batch-orders", batchBody, "category=USDT"));
    }
}

Node.js

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

const API_KEY  = 'your-api-key';
const SECRET   = 'your-secret-key';
const BASE_URL = 'api.toobit.com';

/**
 * V2 JSON body 签名请求。
 * 签名字符串 = query string(不含 signature)+ 原始 JSON body(无分隔符)
 */
function v2JsonRequest(endpoint, body, extraQuery = {}) {
    return new Promise((resolve, reject) => {
        const timestamp = Date.now();
        const query = { timestamp, recvWindow: 5000, ...extraQuery };

        // 序列化 JSON body,必须与实际发送的请求体完全一致
        const jsonBody = JSON.stringify(body);

        const queryStr = Object.entries(query).map(([k, v]) => `${k}=${v}`).join('&');
        const signStr  = queryStr + jsonBody;
        const signature = crypto.createHmac('sha256', SECRET).update(signStr).digest('hex');

        const path = `${endpoint}?${queryStr}&signature=${signature}`;
        const options = {
            hostname: BASE_URL,
            path,
            method: 'POST',
            headers: {
                'X-BB-APIKEY': API_KEY,
                'Content-Type': 'application/json',
                'Content-Length': Buffer.byteLength(jsonBody),
            },
        };

        const req = https.request(options, res => {
            let data = '';
            res.on('data', chunk => data += chunk);
            res.on('end', () => resolve(JSON.parse(data)));
        });
        req.on('error', reject);
        req.write(jsonBody);
        req.end();
    });
}

// 示例 1:下普通单
v2JsonRequest('/api/v2/futures/order', {
    symbol: 'BTC-SWAP-USDT', side: 'BUY', positionSide: 'LONG',
    type: 'LIMIT', newClientOrderId: 'v2_order_001', quantity: '1', price: '30000',
}).then(console.log).catch(console.error);

// 示例 2:批量下单(body 为 JSON 数组)
v2JsonRequest('/api/v2/futures/batch-orders', [
    { symbol: 'BTC-SWAP-USDT', side: 'BUY', positionSide: 'LONG',
      type: 'LIMIT', newClientOrderId: 'batch_001', quantity: '1', price: '30000' },
    { symbol: 'ETH-SWAP-USDT', side: 'SELL', positionSide: 'SHORT',
      type: 'LIMIT', newClientOrderId: 'batch_002', quantity: '2', price: '2000' },
], { category: 'USDT' }).then(console.log).catch(console.error);

Go

go
package main

import (
    "bytes"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "strconv"
    "time"
)

const (
    apiKey  = "your-api-key"
    secret  = "your-secret-key"
    baseURL = "https://api.toobit.com"
)

func hmacSha256(key, data string) string {
    mac := hmac.New(sha256.New, []byte(key))
    mac.Write([]byte(data))
    return hex.EncodeToString(mac.Sum(nil))
}

// v2JsonRequest 发起 V2 JSON body 签名请求。
// 签名字符串 = query string(不含 signature)+ 原始 JSON body(无分隔符)。
// body 可为 map(单笔)或 []map(批量),extraQuery 为额外查询参数如 "category=USDT"。
func v2JsonRequest(endpoint string, body any, extraQuery string) (string, error) {
    jsonBytes, err := json.Marshal(body)
    if err != nil {
        return "", err
    }
    jsonBody := string(jsonBytes)

    timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
    queryStr  := "timestamp=" + timestamp + "&recvWindow=5000"
    if extraQuery != "" {
        queryStr += "&" + extraQuery
    }

    // 签名字符串 = query string + JSON body(无分隔符)
    signature := hmacSha256(secret, queryStr+jsonBody)

    url := baseURL + endpoint + "?" + queryStr + "&signature=" + signature
    req, err := http.NewRequest("POST", url, bytes.NewBufferString(jsonBody))
    if err != nil {
        return "", err
    }
    req.Header.Set("X-BB-APIKEY", apiKey)
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    result, _ := io.ReadAll(resp.Body)
    return string(result), nil
}

func main() {
    // 示例 1:下普通单
    order := map[string]string{
        "symbol": "BTC-SWAP-USDT", "side": "BUY", "positionSide": "LONG",
        "type": "LIMIT", "newClientOrderId": "v2_order_001",
        "quantity": "1", "price": "30000",
    }
    result, err := v2JsonRequest("/api/v2/futures/order", order, "")
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println(result)
    }

    // 示例 2:批量下单(body 为 JSON 数组)
    batch := []map[string]string{
        {"symbol": "BTC-SWAP-USDT", "side": "BUY", "positionSide": "LONG",
            "type": "LIMIT", "newClientOrderId": "batch_001", "quantity": "1", "price": "30000"},
        {"symbol": "ETH-SWAP-USDT", "side": "SELL", "positionSide": "SHORT",
            "type": "LIMIT", "newClientOrderId": "batch_002", "quantity": "2", "price": "2000"},
    }
    result, err = v2JsonRequest("/api/v2/futures/batch-orders", batch, "category=USDT")
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println(result)
    }
}

cURL

bash
#!/bin/bash

API_KEY="your-api-key"
SECRET_KEY="your-secret-key"
BASE_URL="https://api.toobit.com"

# V2 JSON body 签名:签名字符串 = query string(不含 signature)+ 原始 JSON body(无分隔符)
sign_v2_json() {
    local query_str="$1"  # 如 "timestamp=xxx&recvWindow=5000"
    local json_body="$2"  # 原始 JSON 字符串
    echo -n "${query_str}${json_body}" | openssl dgst -sha256 -hmac "$SECRET_KEY" | awk '{print $2}'
}

TIMESTAMP=$(date +%s)000

# 示例 1:下普通单
JSON_BODY='{"symbol":"BTC-SWAP-USDT","side":"BUY","positionSide":"LONG","type":"LIMIT","newClientOrderId":"v2_001","quantity":"1","price":"30000"}'
QUERY_STR="timestamp=${TIMESTAMP}&recvWindow=5000"
SIG=$(sign_v2_json "$QUERY_STR" "$JSON_BODY")

curl -s -X POST "${BASE_URL}/api/v2/futures/order?${QUERY_STR}&signature=${SIG}" \
    -H "X-BB-APIKEY: ${API_KEY}" \
    -H "Content-Type: application/json" \
    -d "${JSON_BODY}"
echo ""

# 示例 2:批量下单(body 为 JSON 数组)
BATCH_BODY='[{"symbol":"BTC-SWAP-USDT","side":"BUY","positionSide":"LONG","type":"LIMIT","newClientOrderId":"batch_001","quantity":"1","price":"30000"},{"symbol":"ETH-SWAP-USDT","side":"SELL","positionSide":"SHORT","type":"LIMIT","newClientOrderId":"batch_002","quantity":"2","price":"2000"}]'
QUERY_STR2="timestamp=${TIMESTAMP}&recvWindow=5000&category=USDT"
SIG2=$(sign_v2_json "$QUERY_STR2" "$BATCH_BODY")

curl -s -X POST "${BASE_URL}/api/v2/futures/batch-orders?${QUERY_STR2}&signature=${SIG2}" \
    -H "X-BB-APIKEY: ${API_KEY}" \
    -H "Content-Type: application/json" \
    -d "${BATCH_BODY}"
echo ""

注意事项

  1. 时间戳:必须使用毫秒级Unix时间戳
  2. 签名格式:签名必须为小写十六进制字符串,区分大小写,验签时请勿使用大写
  3. 签名不包含signature字段:生成签名时,参数字典中不应包含signature字段
  4. 混合参数:当同时使用query string和request body时,签名输入为query string在前,request body在后,中间没有&分隔符
  5. API v2 + JSON 请求体:对 /api/v2/Content-Type: application/jsonPOST(如 POST /api/v2/futures/batch-orders),签名字符串为 query string(含 timestamp 等)与原始 JSON 请求体直接拼接,中间无 &;JSON body 必须使用紧凑格式(不含换行符),服务端按行读取 body 会丢弃换行,发送格式化 JSON 将导致验签失败;参与签名的 JSON 字符串必须与实际发送的请求体字符完全一致。详见基础信息JSON 请求体验签(API v2) 小节

更多信息

基于 MIT 许可发布