import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# 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>
```

:::tip SDK Users
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`) |

:::info About App Secret
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](#step-2-construct-the-key) for details.
:::

## What Gets Signed

The signature is computed from four parts of the HTTP request:

1. Request path
2. Query parameters
3. Request body
4. Signing headers — the following headers participate in signature computation:
   - `x-app-key`
   - `x-signature-algorithm`
   - `x-signature-version`
   - `x-signature-nonce`
   - `x-timestamp`
   - `host`

:::note
`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.
:::

:::caution Important
- The content being signed does **not** require [URL Encoding](https://en.wikipedia.org/wiki/Percent-encoding) at this stage.
- For POST requests, `Content-Type` must be `application/json`.
:::

## Signature Algorithm

### Step 1: Construct the Signature String

1. Merge all query parameters and the signing headers (listed in [What Gets Signed](#what-gets-signed)) into a single list.
2. Sort all parameter names in ascending alphabetical order.
3. Join them as `name1=value1&name2=value2&...` → this is **`str1`**.
4. If the request has a body, compute its MD5 hash and convert to uppercase: `toUpper(MD5(body))` → this is **`str2`**.
5. Concatenate: **`str3`** = `path` + `&` + `str1` + `&` + `str2`
   - If the body is empty: **`str3`** = `path` + `&` + `str1`
6. URL-encode `str3` → this is **`encoded_string`**.

:::caution
- There must be **no** extra spaces between body parameter keys and values.
- If the body is empty, omit `str2` entirely.
:::

### 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:**
```json
{"k1":123,"k2":"this is the api request body","k3":true,"k4":{"foo":[1,2]}}
```

**App Secret:** `0f50a2e853334a9aae1a783bee120c1f`

### Step 1: Construct the Signature String

1. 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
   ```

2. 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
   ```

3. Compute MD5 of the body and convert to uppercase → **str2**:

   ```
   E296C96787E1A309691CEF3692F5EEDD
   ```

4. 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
   ```

5. 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
   ```

:::note
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=`

:::tip Verify Your Implementation
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.

<Tabs groupId="programming-language">
  <TabItem value="python" label="Python" default>

```python
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}")
```

  </TabItem>
  <TabItem value="java" label="Java">

```java
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();
    }
}
```

  </TabItem>
</Tabs>

## 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`):

```go
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.