Signature
Every API request to Webull must include a cryptographic signature in the request header. The signature is computed from the request content and your App Secret, ensuring the integrity and authenticity of each request.
x-signature: <signature_value>
The Webull SDK handles signature generation automatically. If you're using the SDK, you can skip this page — it's here for those implementing signature logic manually.
Required Request Headers
Every API request must include the following headers:
| Header | Required | Description |
|---|---|---|
x-app-key | Yes | A unique identifier issued to a developer for accessing the API |
x-timestamp | Yes | Request timestamp in ISO 8601 format: YYYY-MM-DDThh:mm:ssZ (UTC only) |
x-signature | Yes | The computed signature value (output of the algorithm described below) |
x-signature-algorithm | Yes | Signature algorithm (e.g. HMAC-SHA1) |
x-signature-version | Yes | Signature algorithm version (e.g. 1.0) |
x-signature-nonce | Yes | Unique random string, regenerated for each request |
x-version | Yes | Interface version (accepts v2) |
The app_secret is a unique key issued to developers. It is not included in any HTTP request header — it is used solely on the client side for signature generation. See Step 2: Construct the Key for details.
What Gets Signed
The signature is computed from four parts of the HTTP request:
- Request path
- Query parameters
- Request body
- Signing headers — the following headers participate in signature computation:
x-app-keyx-signature-algorithmx-signature-versionx-signature-noncex-timestamphost
x-signature and x-version do not participate in signing. x-signature carries the output of the signature itself; x-version is a required request header but is excluded from the signature computation.
- The content being signed does not require URL Encoding at this stage.
- For POST requests,
Content-Typemust beapplication/json.
Signature Algorithm
Step 1: Construct the Signature String
- Merge all query parameters and the signing headers (listed in What Gets Signed) into a single list.
- Sort all parameter names in ascending alphabetical order.
- Join them as
name1=value1&name2=value2&...→ this isstr1. - If the request has a body, compute its MD5 hash and convert to uppercase:
toUpper(MD5(body))→ this isstr2. - Concatenate:
str3=path+&+str1+&+str2- If the body is empty:
str3=path+&+str1
- If the body is empty:
- URL-encode
str3→ this isencoded_string.
- There must be no extra spaces between body parameter keys and values.
- If the body is empty, omit
str2entirely.
Step 2: Construct the Key
Append & to the end of your App Secret:
app_secret = "<your_app_secret>&"
Step 3: Generate the Signature
signature = base64(HMAC-SHA1(app_secret, encoded_string))
Worked Example
Below is a complete example showing each step of the signature generation process.
Request Details
Path: /trade/place_order
Query Parameters:
| Name | Value |
|---|---|
| a1 | webull |
| a2 | 123 |
| a3 | xxx |
| q1 | yyy |
Request Headers:
| Name | Value |
|---|---|
| x-app-key | 776da210ab4a452795d74e726ebd74b6 |
| x-timestamp | 2022-01-04T03:55:31Z |
| x-signature-version | 1.0 |
| x-signature-algorithm | HMAC-SHA1 |
| x-signature-nonce | 48ef5afed43d4d91ae514aaeafbc29ba |
| host | api.webull.com |
Body:
{"k1":123,"k2":"this is the api request body","k3":true,"k4":{"foo":[1,2]}}
App Secret: 0f50a2e853334a9aae1a783bee120c1f
Step 1: Construct the Signature String
-
Merge query parameters and signing headers into a single list, then sort all parameter names in ascending alphabetical 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 -
Join them as key=value pairs with
&→ 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 -
Compute MD5 of the body and convert to uppercase → str2:
E296C96787E1A309691CEF3692F5EEDD -
Concatenate 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 -
URL-encode str3 → 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
The worked example merges algorithm steps 1–3 into a single step for readability. The logic is identical to the 6-step algorithm above.
Step 2: Construct the Key
app_secret = "0f50a2e853334a9aae1a783bee120c1f&"
Step 3: Generate the Signature
signature = base64(HMAC-SHA1(app_secret, encoded_string))
Result: kvlS6opdZDhEBo5jq40nHYXaLvM=
Use the values above to test your signature code. If your output matches kvlS6opdZDhEBo5jq40nHYXaLvM=, your implementation is correct.
Code Examples
The following examples demonstrate how to sign and call the Account List API (GET /openapi/account/list) without using the Webull SDK.
- Python
- Java
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}")
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.*;
public class AccountListExample {
// Replace with your credentials
static final String APP_KEY = "<your_app_key>";
static final String APP_SECRET = "<your_app_secret>";
static final String HOST = "<api_endpoint>"; // Your API host, varies by environment
public static void main(String[] args) throws Exception {
String path = "/openapi/account/list";
String timestamp = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
.withZone(ZoneOffset.UTC)
.format(Instant.now());
String nonce = UUID.randomUUID().toString().replace("-", "");
String signature = generateSignature(
path, Collections.emptyMap(), null,
APP_KEY, APP_SECRET, HOST, timestamp, nonce
);
// Build request
URL url = new URL("https://" + HOST + path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("x-app-key", APP_KEY);
conn.setRequestProperty("x-timestamp", timestamp);
conn.setRequestProperty("x-signature", signature);
conn.setRequestProperty("x-signature-algorithm", "HMAC-SHA1");
conn.setRequestProperty("x-signature-version", "1.0");
conn.setRequestProperty("x-signature-nonce", nonce);
conn.setRequestProperty("x-version", "v2");
// Read response
int status = conn.getResponseCode();
BufferedReader reader = new BufferedReader(new InputStreamReader(
status == 200 ? conn.getInputStream() : conn.getErrorStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
System.out.println("Status: " + status);
System.out.println("Response: " + response);
}
/**
* Generate the request signature following the 3-step algorithm.
*/
public static String generateSignature(
String path, Map<String, String> queryParams, String bodyString,
String appKey, String appSecret, String host,
String timestamp, String nonce) throws Exception {
// Signing headers (x-signature and x-version are NOT included)
Map<String, String> allParams = new TreeMap<>(); // TreeMap sorts by key
allParams.putAll(queryParams);
allParams.put("x-app-key", appKey);
allParams.put("x-timestamp", timestamp);
allParams.put("x-signature-algorithm", "HMAC-SHA1");
allParams.put("x-signature-version", "1.0");
allParams.put("x-signature-nonce", nonce);
allParams.put("host", host);
// Step 1: Construct the Signature String
StringJoiner joiner = new StringJoiner("&");
for (Map.Entry<String, String> entry : allParams.entrySet()) {
joiner.add(entry.getKey() + "=" + entry.getValue());
}
String str1 = joiner.toString();
String str3;
if (bodyString != null && !bodyString.isEmpty()) {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(bodyString.getBytes(StandardCharsets.UTF_8));
String str2 = bytesToHex(digest).toUpperCase();
str3 = path + "&" + str1 + "&" + str2;
} else {
str3 = path + "&" + str1;
}
String encodedString = URLEncoder.encode(str3, StandardCharsets.UTF_8);
// Step 2: Construct the Key
String signingKey = appSecret + "&";
// Step 3: Generate the Signature
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(new SecretKeySpec(signingKey.getBytes(StandardCharsets.UTF_8), "HmacSHA1"));
byte[] rawSignature = mac.doFinal(encodedString.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(rawSignature);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
Edge Cases
Duplicate Parameter Names
If a request contains multiple parameters with the same name, sort all values in ascending order and join them with &, then use the combined value in str1:
# URL: /path?name1=value1&name1=value2&name1=value3
# After sorting values in ascending order:
name1 = value1&value2&value3
# This combined value participates in str1 as:
# name1=value1&value2&value3
In other words, the duplicate keys are merged into a single name1=... entry in the sorted parameter list, with all values joined by &.
JSON Body Serialization
When computing the MD5 hash of the request body, ensure the JSON string has no extra spaces between keys and values (use compact serialization like separators=(',', ':') in Python or equivalent in your language).
Additionally, the JSON body used for MD5 computation must be exactly the same string sent in the HTTP request body. If you use json=body in Python's requests.post(), the library serializes the body internally and may produce a different string than what you computed the MD5 from. Always serialize the body yourself (e.g., json.dumps(body, separators=(',', ':'))) and pass it as data=body_string with Content-Type: application/json.
Language-Specific HTML Escaping
Some languages automatically escape special characters in JSON output. You must reverse these escapes before computing the body MD5. For example:
Go — json.Marshal escapes <, >, and & by default (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
}
If your language or framework has similar behavior, ensure the raw JSON (without HTML escaping) is used for signature computation.