跳至主要内容

簽名

每個發送至 Webull 的 API 請求都必須在請求標頭中包含加密簽名。簽名由請求內容和 App Secret 計算生成,確保每個請求的完整性和真實性。

x-signature: <signature_value>
SDK 用戶

Webull SDK 會自動處理簽名生成。如果您使用 SDK,可以跳過本頁 — 本文適用於需要手動實現簽名邏輯的開發者。

必要的請求標頭

每個 API 請求必須包含以下標頭:

標頭必填說明
x-app-key發給開發者用於存取 API 的唯一識別碼
x-timestamp請求時間戳,ISO 8601 格式:YYYY-MM-DDThh:mm:ssZ(僅限 UTC)
x-signature計算生成的簽名值(即下方演算法的輸出結果)
x-signature-algorithm簽名演算法(如 HMAC-SHA1
x-signature-version簽名演算法版本(如 1.0
x-signature-nonce唯一隨機字串,每次請求重新生成
x-version介面版本(接受 v2
關於 App Secret

app_secret 是發給開發者的唯一密鑰。它不會包含在任何 HTTP 請求標頭中 — 僅在客戶端用於簽名生成。詳見步驟 2:構建密鑰

簽名內容

簽名由 HTTP 請求的四個部分計算而成:

  1. 請求路徑
  2. 查詢參數
  3. 請求主體
  4. 簽名標頭 — 以下標頭參與簽名計算:
  • x-app-key
  • x-signature-algorithm
  • x-signature-version
  • x-signature-nonce
  • x-timestamp
  • host
備註

x-signaturex-version 不參與簽名計算。x-signature 承載簽名本身的輸出結果;x-version 是必要的請求標頭,但不納入簽名計算。

重要
  • 參與簽名的內容在此階段不需要 URL 編碼
  • POST 請求的 Content-Type 必須為 application/json

簽名演算法

步驟 1:構建簽名字串

  1. 將所有查詢參數和簽名標頭(見簽名內容中的列表)合併為一個列表。
  2. 按參數名稱的字母升序排列。
  3. name1=value1&name2=value2&... 格式拼接 → 得到 str1
  4. 如果請求有主體,計算其 MD5 雜湊值並轉為大寫:toUpper(MD5(body)) → 得到 str2
  5. 拼接:str3 = path + & + str1 + & + str2
  • 如果主體為空:str3 = path + & + str1
  1. str3 進行 URL 編碼 → 得到 encoded_string
警告
  • 主體參數的鍵和值之間不得有多餘空格。
  • 如果主體為空,完全省略 str2

步驟 2:構建密鑰

在 App Secret 末尾附加字元 &

app_secret = "<your_app_secret>&"

步驟 3:生成簽名

signature = base64(HMAC-SHA1(app_secret, encoded_string))

完整範例

以下是簽名生成過程的完整範例。

請求詳情

路徑: /trade/place_order

查詢參數:

名稱
a1webull
a2123
a3xxx
q1yyy

請求標頭:

名稱
x-app-key776da210ab4a452795d74e726ebd74b6
x-timestamp2022-01-04T03:55:31Z
x-signature-version1.0
x-signature-algorithmHMAC-SHA1
x-signature-nonce48ef5afed43d4d91ae514aaeafbc29ba
hostapi.webull.com

主體:

{"k1":123,"k2":"this is the api request body","k3":true,"k4":{"foo":[1,2]}}

App Secret: 0f50a2e853334a9aae1a783bee120c1f

步驟 1:構建簽名字串

  1. 將查詢參數和簽名標頭合併為一個列表,按參數名稱的字母升序排列:

    a1=webull, a2=123, a3=xxx,
    host=api.webull.com,
    q1=yyy,
    x-app-key=776da210ab4a452795d74e726ebd74b6,
    x-signature-algorithm=HMAC-SHA1,
    x-signature-nonce=48ef5afed43d4d91ae514aaeafbc29ba,
    x-signature-version=1.0,
    x-timestamp=2022-01-04T03:55:31Z
  2. 以 key=value 格式用 & 拼接 → str1

    a1=webull&a2=123&a3=xxx&host=api.webull.com&q1=yyy&x-app-key=776da210ab4a452795d74e726ebd74b6&x-signature-algorithm=HMAC-SHA1&x-signature-nonce=48ef5afed43d4d91ae514aaeafbc29ba&x-signature-version=1.0&x-timestamp=2022-01-04T03:55:31Z
  3. 計算主體的 MD5 並轉為大寫 → str2

    E296C96787E1A309691CEF3692F5EEDD
  4. 拼接 path + & + str1 + & + str2 → str3

    /trade/place_order&a1=webull&a2=123&a3=xxx&host=api.webull.com&q1=yyy&x-app-key=776da210ab4a452795d74e726ebd74b6&x-signature-algorithm=HMAC-SHA1&x-signature-nonce=48ef5afed43d4d91ae514aaeafbc29ba&x-signature-version=1.0&x-timestamp=2022-01-04T03:55:31Z&E296C96787E1A309691CEF3692F5EEDD
  5. 對 str3 進行 URL 編碼 → encoded_string

    %2Ftrade%2Fplace_order%26a1%3Dwebull%26a2%3D123%26a3%3Dxxx%26host%3Dapi.webull.com%26q1%3Dyyy%26x-app-key%3D776da210ab4a452795d74e726ebd74b6%26x-signature-algorithm%3DHMAC-SHA1%26x-signature-nonce%3D48ef5afed43d4d91ae514aaeafbc29ba%26x-signature-version%3D1.0%26x-timestamp%3D2022-01-04T03%3A55%3A31Z%26E296C96787E1A309691CEF3692F5EEDD
  6. When path is empty (e.g., gRPC streaming subscriptions):

    str3 = name1=value1=name2=value2=... + & + str2
    If the body is empty: str3 = name1=value1=name2=value2=...
    Note: When there is no path, the sorted key=value pairs are joined with = instead of &.
備註

完整範例將演算法步驟 1–3 合併為一步以提高可讀性,邏輯與上方 6 步演算法完全一致。

步驟 2:構建密鑰

app_secret = "0f50a2e853334a9aae1a783bee120c1f&"

步驟 3:生成簽名

signature = base64(HMAC-SHA1(app_secret, encoded_string))

結果: kvlS6opdZDhEBo5jq40nHYXaLvM=

驗證您的實作

使用上述數值測試您的簽名程式碼。如果您的輸出結果與 kvlS6opdZDhEBo5jq40nHYXaLvM= 一致,則表示您的實作正確。

程式碼範例

以下範例示範如何在不使用 Webull SDK 的情況下,對 Account List API(GET /openapi/account/list)進行簽名並發起呼叫。

import hashlib
import hmac
import base64
import json
import uuid
import urllib.parse
from datetime import datetime, timezone

import requests

# Replace with your credentials
APP_KEY = "<your_app_key>"
APP_SECRET = "<your_app_secret>"
HOST = "<api_endpoint>" # Your API host, varies by environment
BASE_URL = f"https://{HOST}"


def generate_signature(path, query_params, body_string, app_key, app_secret, host, timestamp, nonce):
"""
Generate the request signature following the 3-step algorithm.
"""
# Signing headers (x-signature and x-version are NOT included)
signing_headers = {
"x-app-key": app_key,
"x-timestamp": timestamp,
"x-signature-algorithm": "HMAC-SHA1",
"x-signature-version": "1.0",
"x-signature-nonce": nonce,
"host": host,
}

# Step 1: Construct the Signature String
# 1. Merge query params + signing headers
all_params = {}
all_params.update(query_params)
all_params.update(signing_headers)

# 2-3. Sort by key, join as key=value pairs → str1
str1 = "&".join(f"{k}={all_params[k]}" for k in sorted(all_params.keys()))

# 4. If body exists, compute MD5 (uppercase hex) → str2
if body_string:
str2 = hashlib.md5(body_string.encode("utf-8")).hexdigest().upper()
str3 = f"{path}&{str1}&{str2}"
else:
str3 = f"{path}&{str1}"

# 6. URL-encode str3
encoded_string = urllib.parse.quote(str3, safe="")

# Step 2: Construct the Key
signing_key = f"{app_secret}&"

# Step 3: Generate the Signature
signature = base64.b64encode(
hmac.new(signing_key.encode("utf-8"), encoded_string.encode("utf-8"), hashlib.sha1).digest()
).decode("utf-8")

return signature


def call_api(method, path, query_params=None, body=None):
"""
Sign and send an API request.
"""
query_params = query_params or {}
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
nonce = uuid.uuid4().hex

# Serialize body as compact JSON (no spaces) — the exact same string
# must be used for both MD5 computation and the HTTP request body.
body_string = json.dumps(body, separators=(",", ":")) if body else None

signature = generate_signature(
path, query_params, body_string,
APP_KEY, APP_SECRET, HOST, timestamp, nonce,
)

headers = {
"x-app-key": APP_KEY,
"x-timestamp": timestamp,
"x-signature": signature,
"x-signature-algorithm": "HMAC-SHA1",
"x-signature-version": "1.0",
"x-signature-nonce": nonce,
"x-version": "v2",
}

url = f"{BASE_URL}{path}"

if method.upper() == "GET":
resp = requests.get(url, headers=headers, params=query_params)
else:
headers["Content-Type"] = "application/json"
# Pass body_string as data= (not json=) to avoid re-serialization
resp = requests.post(url, headers=headers, data=body_string)

return resp


# --- Call Account List ---
resp = call_api("GET", "/openapi/account/list")
print(f"Status: {resp.status_code}")
if resp.status_code == 200:
for account in resp.json():
print(f" Account ID: {account['account_id']}, Type: {account['account_type']}")
else:
print(f"Error: {resp.text}")

邊界情況

重複參數名稱

如果請求中包含多個同名參數,按值的升序排列後以 & 拼接,然後將合併後的值用於 str1

# URL: /path?name1=value1&name1=value2&name1=value3
# 按值升序排列後:
name1 = value1&value2&value3

# 合併後的值在 str1 中的形式:
# name1=value1&value2&value3

即重複的鍵合併為一個 name1=... 條目,所有值以 & 拼接。

JSON 主體序列化

計算請求主體的 MD5 雜湊值時,確保 JSON 字串的鍵和值之間沒有多餘空格(使用緊湊序列化,如Python 中的 separators=(',', ':'),或其他語言的等效方式)。

此外,用於計算 MD5 的 JSON 請求內容(Body)必須與實際 HTTP 請求中發送的內容完全一致。如果您在 Python 的 requests.post() 中使用 json=body,函式庫會在內部自行序列化請求內容,而產生的字串可能與您計算 MD5 時使用的字串不同。建議始終自行序列化請求內容(例如使用 json.dumps(body, separators=(',', ':'))),並透過 data=body_string 搭配 Content-Type: application/json 發送請求,以確保 MD5 計算結果與實際請求內容保持一致。

語言特定的 HTML 轉義

部分程式語言會自動轉義 JSON 輸出中的特殊字元。計算主體 MD5 前必須還原這些轉義。例如:

Go — json.Marshal 預設會轉義 <>&escapeHtml = true):

func unescapeJSON(data []byte) []byte {
data = bytes.Replace(data, []byte("\\u0026"), []byte("&"), -1)
data = bytes.Replace(data, []byte("\\u003c"), []byte("<"), -1)
data = bytes.Replace(data, []byte("\\u003e"), []byte(">"), -1)
return data
}

如果您使用的語言或框架有類似行為,請確保使用原始 JSON(未經 HTML 轉義)進行簽名計算。