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"
    "strings"
)

// GenerateSignature 生成HMAC SHA256签名
func GenerateSignature(secretKey string, params map[string]string) string {
    // 拼接query string
    var parts []string
    for k, v := range params {
        parts = append(parts, fmt.Sprintf("%s=%s", k, v))
    }
    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.HashMap;
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 HashMap<>();
        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 HashMap<>();
        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)
    keys = c.getParamKeys(params)
    
    // 构建query string(与签名时使用相同的keys顺序)
    queryString := c.buildQueryString(params, keys)
    
    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
    params="${params}&timestamp=${timestamp}"
    
    # 添加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"

注意事项

  1. 时间戳:必须使用毫秒级Unix时间戳
  2. 签名不包含signature字段:生成签名时,参数字典中不应包含signature字段
  3. 混合参数:当同时使用query string和request body时,签名输入为query string在前,request body在后,中间没有&分隔符

更多信息

基于 MIT 许可发布