Asynchronous · Thread-Safe · Header-Light · Cryptographically Signed
Built on libcurl · nlohmann/json · OpenSSL · spdlog
- Overview
- Prerequisites
- Installation
- Configuration
- Architecture
- Core API Reference —
wdk::core - Utilities API Reference —
wdk::utilities - Client API Reference —
wdk::client - Data Layer Reference —
wdk::data - Usage Guide
- Error Handling
- Build System Reference
- License
WDK (Webull Developer Kit) is a native C++23 client library for the Webull OpenAPI. It provides fully typed, asynchronous access to market data and trading operations through a three-layer architecture that separates transport, signing, and business logic.
The library was designed with the following principles:
- Zero dynamic dispatch in the hot path. All client objects are instantiated by the caller; no virtual tables are used in the core I/O path.
- Cooperative ownership. All heavyweight resources (
CurlPool,ThreadPool,Token) are managed throughstd::unique_ptrby the consuming application. Clients receive non-owning references. - Symmetric async/sync surface. Every client method has both a blocking synchronous variant and a
std::future-returning asynchronous variant. The caller selects the scheduling model. - Request signing encapsulated from business logic. HMAC-SHA1 signing, nonce generation, and UTC timestamping are performed inside
wdk::utilities::execute_requestand are never visible to the calling code.
| Requirement | Minimum Version |
|---|---|
| GCC | 13.0 |
| Clang | 17.0 |
| C++ Standard | C++23 (-std=c++23) |
| Tool | Purpose |
|---|---|
| CMake | Build system generator |
| Ninja | Recommended build backend |
| Library | Purpose | Notes |
|---|---|---|
libcurl |
HTTP transport | With HTTPS / HTTP/2 support |
OpenSSL |
HMAC-SHA1, MD5, Base64 | libcrypto is used |
spdlog |
Structured logging | Header-only mode supported |
nlohmann/json |
JSON serialization | Bundled under lib/ |
All dependencies except nlohmann/json must be installed system-wide and discoverable by CMake's find_package.
On a Debian/Ubuntu system:
sudo apt install libcurl4-openssl-dev libssl-dev libspdlog-dev cmake ninja-buildOn Arch Linux:
sudo pacman -S curl openssl spdlog cmake ninjaClone the repository and use the provided build script:
git clone https://github.com/Pooh555/Webull-SDK.git
cd Webull-SDK
./build.shThe build script accepts the following flags:
| Flag | Description | Default |
|---|---|---|
-b, --build-type <type> |
CMake build type: Debug, Release, RelWithDebInfo |
Debug |
-c, --clean |
Remove the existing build directory before building | Off |
-h, --help |
Print the help menu and exit | — |
Example: release build
./build.sh --build-type ReleaseExample: clean debug build
./build.sh --cleanUpon successful completion, the following artifacts are produced:
| Artifact | Location |
|---|---|
| Static library | out/build/<preset>/libWebull-SDK.a |
| Demo binary | examples/bin/Webull-SDK-Demo |
To execute the bundled demo:
./run.shWDK exposes a CMake target Webull::SDK for downstream consumption.
Step 1. Add the repository as a Git submodule:
git submodule add https://github.com/Pooh555/Webull-SDK.git third-party/Webull-SDK
git submodule update --init --recursiveStep 2. Configure your project's CMakeLists.txt:
add_subdirectory(third-party/Webull-SDK)
add_executable(MyApplication
src/main.cpp
)
target_link_libraries(MyApplication
PRIVATE
Webull::SDK
)Step 3. Call cmake --build as usual. The Webull::SDK target propagates all required include directories and compile options transitively.
The SDK authenticates requests using an application identity issued by Webull. Credentials are stored in a JSON file and loaded at startup via wdk::core::Credentials.
Default location: examples/res/credentials.json
{
"id": "<your-app-id>",
"key": "<your-app-key>",
"secret": "<your-app-secret>"
}| Field | Type | Description |
|---|---|---|
id |
string |
Application identifier (used for x-app-id context) |
key |
string |
Public application key (sent as x-app-key header) |
secret |
string |
Private signing secret (used for HMAC-SHA1 key derivation) |
The
secretfield is never transmitted over the network. It is used exclusively to derive the HMAC-SHA1 signing key of the form<secret>&.
Session tokens are persisted to disk to avoid re-authentication on each restart. Once a token has been approved and activated, it is written back to this file automatically.
Default location: examples/res/token.json
{
"token": "<session-token-string>"
}If the stored token fails verification at startup, the SDK will automatically request a new token and block until the user approves the login via the Webull mobile application. The newly activated token is then written back to this file.
WDK supports both the UAT (test) environment and the production environment. The active endpoint is selected in application.hpp:
// UAT (test) endpoint
static constexpr std::string_view HOST { "th-api.uat.webullbroker.com" };
// Production endpoint
static constexpr std::string_view HOST { "api.webull.co.th" };Ensure you are targeting the UAT endpoint for all development and testing. Requests to the production endpoint will execute against live accounts.
The codebase is organized into three discrete namespaces, each with a singular responsibility:
┌──────────────────────────────────────────────────────────────────────┐
│ wdk::client │
│ TradingClient │ MarketClient │
│ (orders · accounts · instruments) │ (tick · snapshot · OHLCV) │
└───────────────┬──────────────────────────────────────┬──────────────┘
│ │
┌───────────────▼──────────────────────────────────────▼──────────────┐
│ wdk::utilities │
│ http (execute_request) │ openapi (signing) │ cryptography │
│ json (read/write) │ time (UTC stamps) │ │
└───────────────┬──────────────────────────────────────────────────────┘
│
┌───────────────▼──────────────────────────────────────────────────────┐
│ wdk::core │
│ Credentials │ Token │ CurlPool │ ThreadPool │ RateLimiter │
└──────────────────────────────────────────────────────────────────────┘
wdk::core — Infrastructure primitives. This layer manages all stateful, long-lived resources: connection pools, worker threads, session tokens, and application credentials. Nothing in this layer is aware of business-domain concepts.
wdk::utilities — Stateless functional utilities. All HTTP dispatch, HMAC-SHA1 signing, nonce generation, UTC timestamping, and JSON I/O reside here. Functions in this layer are pure or near-pure; they take all required inputs as parameters and return results.
wdk::client — Business domain clients. MarketClient and TradingClient hold non-owning references to wdk::core primitives, construct typed request objects, and delegate I/O to wdk::utilities. This layer contains no I/O logic of its own.
wdk::data — Typed response model. Structured C++ types representing API responses, and converter functions that parse raw Response objects into those types.
All I/O operations are dispatched through wdk::core::ThreadPool, which maintains a pool of std::jthread workers. Each worker processes tasks from a shared std::queue<std::move_only_function<void()>>.
Callers receive std::future<wdk::utilities::Response> objects. Futures are fulfilled on whichever worker thread executes the corresponding task.
CurlPool provides a blocking acquire() method: if all handles are in use, the calling thread waits on a std::condition_variable until a handle is returned. This provides implicit back-pressure against request bursts that would otherwise saturate the connection pool.
All CURL handles in the pool share a single CURLSH* handle configured to cache DNS resolutions and SSL sessions across connections. This eliminates redundant TLS handshakes and DNS round-trips when multiple requests target the same host concurrently.
HTTP/2 multiplexing is enabled per handle via CURL_HTTP_VERSION_2TLS. TCP keep-alive is configured with an idle timeout of 60 seconds and a probe interval of 30 seconds.
Application startup
│
▼
Token::Token(token_path, pool, credentials, host)
│
├── Load token from disk
│
├── Token::verify()
│ │
│ ├── is_valid() && status == "NORMAL" ──► Return (token is active)
│ │
│ └── Otherwise ─────────────────────────► Token::generate()
│ │
│ ▼
│ POST /openapi/auth/token/create
│ │
│ ▼
│ Poll Token::verify() every 5s
│ until status != "PENDING"
│ │
│ ▼
│ Persist token to disk
│
└── Throw std::runtime_error if token fails to reach "NORMAL"
The Token constructor is blocking by design: the application cannot proceed until a valid, activated session token is available. This guarantees that all subsequent client operations have a usable token handle.
Each outgoing request is signed using the Webull OpenAPI HMAC-SHA1 scheme. The signing procedure is encapsulated in wdk::utilities::generate_signature and proceeds as follows:
- Assemble a canonical parameter set comprising the fixed protocol headers (
host,x-app-key,x-signature-algorithm,x-signature-nonce,x-signature-version,x-timestamp) merged with any query string parameters extracted from the request path. - Sort the parameter set lexicographically by key.
- Serialize the sorted set to a
key=value&key=valuestring. - Prepend the request path to form the sign string:
<path>&<canonical>. - If a request body is present, append
&<MD5(body)>(uppercase hex). - URL-encode the complete sign string using
curl_easy_escape. - Derive the signing key as
<app_secret>&. - Compute
HMAC-SHA1(signing_key, url_encoded_sign_string)and Base64-encode the result.
The signature is transmitted in the x-signature HTTP header. The nonce is a 26-character numeric string generated by a thread-local Mersenne Twister (std::mt19937_64) seeded from std::random_device.
Webull-SDK/
├── include/
│ ├── client/
│ │ ├── market.hpp # MarketClient declaration
│ │ └── trading.hpp # TradingClient, OrderRequest, QueryRequest
│ ├── core/
│ │ ├── credentials.hpp # Credentials
│ │ ├── curl_pool.hpp # CurlPool
│ │ ├── rate_limiter.hpp # RateLimiter
│ │ ├── thread_pool.hpp # ThreadPool
│ │ └── token.hpp # Token
│ ├── data/
│ │ └── data.hpp # Typed response structs + converters
│ └── utilities/
│ ├── cryptography.hpp # HMAC-SHA1, MD5, nonce
│ ├── http.hpp # execute_request, Response, HttpMethod
│ ├── json.hpp # read, write, field extractors
│ ├── openapi.hpp # generate_signature
│ └── time.hpp # get_utc_timestamp
├── src/
│ ├── client/
│ │ ├── market.cpp
│ │ └── trading.cpp
│ ├── core/
│ │ ├── credentials.cpp
│ │ ├── curl_pool.cpp
│ │ ├── rate_limiter.cpp
│ │ ├── thread_pool.cpp
│ │ └── token.cpp
│ ├── data/
│ │ └── data.cpp
│ └── utilities/
│ ├── cryptography.cpp
│ ├── http.cpp
│ ├── json.cpp
│ ├── openapi.cpp
│ └── time.cpp
├── examples/
│ ├── bin/ # Compiled demo binary
│ ├── res/
│ │ ├── credentials.json # Application credentials (not committed)
│ │ └── token.json # Session token cache (not committed)
│ └── src/
│ └── core/
│ └── application.cpp # Demo application
├── lib/
│ └── nlohmann/ # Bundled nlohmann/json
├── CMakeLists.txt
├── CMakePresets.json
├── build.sh
└── run.sh
Header: <core/credentials.hpp>
Loads and holds the application identity read from a JSON credentials file. The object is immutable after construction.
class Credentials {
public:
explicit Credentials(const std::filesystem::path& credentials_path);
[[nodiscard]] const std::string& get_id() const;
[[nodiscard]] const std::string& get_key() const;
[[nodiscard]] const std::string& get_secret() const;
};Constructor
Credentials(const std::filesystem::path& credentials_path)
Reads the JSON file at credentials_path and extracts the id, key, and secret fields. Logs a critical error and throws std::runtime_error if the file cannot be parsed.
Member Functions
| Function | Return Type | Description |
|---|---|---|
get_id() |
const std::string& |
Returns the application identifier |
get_key() |
const std::string& |
Returns the public application key |
get_secret() |
const std::string& |
Returns the private signing secret |
Credentials is copyable. Copy semantics are meaningful when multiple subsystems require independent access to credential data.
Header: <core/token.hpp>
Manages the lifecycle of a Webull API session token. Handles token verification, automatic re-generation when expired, and persistence to disk.
class Token {
public:
Token(
const std::filesystem::path& token_path,
CurlPool& pool,
const Credentials& credentials,
const std::string_view& host);
void generate(CurlPool& pool, const Credentials& credentials, const std::string_view& host);
void verify (CurlPool& pool, const Credentials& credentials, const std::string_view& host);
[[nodiscard]] std::string get_handle() const;
[[nodiscard]] std::string get_status() const;
[[nodiscard]] bool is_valid() const;
};Constructor
Token(token_path, pool, credentials, host)
This constructor is blocking. It performs the following sequence synchronously:
- Load any existing token from
token_path. - Call
verify(). If the token is valid and its status is"NORMAL", return immediately. - Otherwise, call
generate()to request a new token from the API. - Poll
verify()every 5 seconds while the status is"PENDING", logging a prompt to approve the login in the Webull mobile application. - On successful activation, persist the new token to
token_path. - If activation fails, throw
std::runtime_error.
Token is move-constructible but not copyable.
Member Functions
| Function | Return Type | Description |
|---|---|---|
generate(pool, credentials, host) |
void |
Issues a token creation request (POST /openapi/auth/token/create) and stores the returned token and status |
verify(pool, credentials, host) |
void |
Issues a token verification request (POST /openapi/auth/token/check) and updates the internal status |
get_handle() |
std::string |
Returns the raw token string for use in x-access-token headers |
get_status() |
std::string |
Returns the current token status ("NORMAL", "PENDING", etc.) |
is_valid() |
bool |
Returns true if the token string is non-empty |
Token Lifecycle Endpoints
| Endpoint | Method | Description |
|---|---|---|
/openapi/auth/token/create |
POST | Request a new session token |
/openapi/auth/token/check |
POST | Verify and refresh the status of an existing token |
Header: <core/curl_pool.hpp>
A bounded pool of reusable CURL* handles. Handles are reset and reconfigured upon each acquisition. All handles share a common CURLSH* for DNS and SSL session caching.
class CurlPool {
public:
using CurlReleaser = std::function<void(CURL*)>;
using CurlHandle = std::unique_ptr<CURL, CurlReleaser>;
explicit CurlPool(size_t pool_size = 10uz);
~CurlPool();
[[nodiscard]] CurlHandle acquire();
};Constructor
CurlPool(size_t pool_size = 10)
Allocates pool_size CURL easy handles and a shared handle configured to cache DNS resolutions and SSL sessions. Logs a critical error for any handle that fails to initialize.
Member Functions
| Function | Return Type | Description |
|---|---|---|
acquire() |
CurlHandle |
Blocks until a handle is available, resets and reconfigures it, and returns it wrapped in a unique_ptr with an auto-release deleter |
The CurlHandle returned by acquire() is an std::unique_ptr<CURL, CurlReleaser>. When this unique_ptr goes out of scope, the deleter calls CurlPool::release(), returning the raw handle to the pool and notifying one waiting thread.
Each acquired handle is configured with:
CURLOPT_TCP_KEEPALIVE: enabledCURLOPT_TCP_KEEPIDLE: 60 secondsCURLOPT_TCP_KEEPINTVL: 30 secondsCURLOPT_HTTP_VERSION:CURL_HTTP_VERSION_2TLSCURLOPT_SHARE: the sharedCURLSH*handle
CurlPool is non-copyable and non-movable.
Header: <core/thread_pool.hpp>
A general-purpose, fixed-size task executor. Workers are std::jthread instances that consume tasks from a shared queue.
class ThreadPool {
public:
explicit ThreadPool(size_t threads = std::thread::hardware_concurrency());
~ThreadPool();
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args) -> std::future<std::invoke_result_t<F, Args...>>;
};Constructor
ThreadPool(size_t threads = std::thread::hardware_concurrency())
Spawns threads worker threads. Each thread waits on a std::condition_variable until a task is available or a stop is requested.
Member Functions
enqueue(F&& f, Args&&... args) -> std::future<...>
Wraps the callable f and its arguments in a std::packaged_task, pushes it onto the task queue, notifies one worker, and returns the associated std::future. Throws std::runtime_error if called after the pool has been stopped.
Destructor
Sets the internal stop_ flag, notifies all workers via the condition variable, and allows the std::jthread destructors to join each worker via their cooperative stop tokens.
ThreadPool is non-copyable and non-movable.
Header: <core/rate_limiter.hpp>
A token-bucket rate limiter suitable for enforcing API call quotas. The bucket refills to max_tokens at the configured refill_interval.
class RateLimiter {
public:
RateLimiter(size_t max_tokens, std::chrono::milliseconds refill_interval);
void acquire(size_t tokens = 1uz);
};Constructor
RateLimiter(size_t max_tokens, std::chrono::milliseconds refill_interval)
Initializes the bucket to full capacity. Sets next_refill_ to now() + refill_interval.
Member Functions
| Function | Description |
|---|---|
acquire(size_t tokens = 1) |
Blocks the calling thread until tokens tokens are available. If the bucket is depleted, the thread waits until next_refill_ and replenishes the bucket to max_tokens. |
Header: <utilities/http.hpp>
enum class HttpMethod : bool {
GET = false,
POST = true
};struct Response {
long http_code { 0L };
std::string message { "" };
};| Field | Type | Description |
|---|---|---|
http_code |
long |
HTTP response status code, or a CURLcode error value on transport failure |
message |
std::string |
Raw response body as received from the server |
[[nodiscard]] Response execute_request(
wdk::core::CurlPool& pool,
const wdk::core::Credentials& credentials,
std::string_view host,
std::string_view path,
HttpMethod method,
std::string_view body_str = "",
std::string_view token = "");The primary I/O function. Performs the following steps on each invocation:
- Acquire a handle from
pool(blocking if all handles are in use). - Generate a UTC timestamp and a 26-character numeric nonce.
- Parse query parameters from
pathto construct the signing parameter set. - Compute the HMAC-SHA1 signature via
generate_signature. - Construct the full URL and configure the CURL handle.
- Build HTTP headers via
generate_headers(wrapped in a scopedunique_ptrfor automaticcurl_slist_free_all). - Execute the request with
curl_easy_perform. - On HTTP 429, apply exponential backoff (
500ms × 2^attempt) and retry up to 3 times. - Return the
Responsecontaining the status code and body.
Retry Behavior
| Condition | Action |
|---|---|
| HTTP 429 (Rate Limited) | Backoff 500ms, 1000ms, 2000ms; retry up to 3 times |
| HTTP 200 | Return immediately |
| Any other HTTP code | Return with the received code and body |
CURLE_* error |
Return with the CURLcode and a JSON error body |
[[nodiscard]] curl_slist* generate_headers(
const wdk::core::Credentials& credentials,
std::string_view timestamp = "",
std::string_view nonce = "",
std::string_view signature = "",
std::string_view token = "");Constructs and returns a curl_slist* containing all required Webull API headers. The caller is responsible for freeing this list via curl_slist_free_all.
In practice, execute_request wraps the raw pointer in a unique_ptr with a custom deleter, so callers of the high-level API never manage this memory directly.
Headers Produced
| Header | Value |
|---|---|
Accept |
application/json |
Content-Type |
application/json |
User-Agent |
WebullBot/1.0 (C++23 Client) |
x-app-key |
credentials.get_key() |
x-timestamp |
ISO 8601 UTC timestamp |
x-signature-version |
1.0 |
x-signature-algorithm |
HMAC-SHA1 |
x-signature-nonce |
26-character numeric nonce |
x-access-token |
Session token (omitted if empty) |
x-version |
v2 |
x-signature |
Base64-encoded HMAC-SHA1 signature |
Header: <utilities/cryptography.hpp>
[[nodiscard]] std::string compute_hmac_sha1(std::string_view key, std::string_view message);Computes the HMAC-SHA1 of message using key and returns the result as a Base64-encoded string (no line breaks). Uses OpenSSL's HMAC() and a BIO_f_base64 chain with BIO_FLAGS_BASE64_NO_NL.
[[nodiscard]] std::string compute_md5(std::string_view data);Computes the MD5 digest of data using OpenSSL's EVP interface and returns the result as an uppercase hexadecimal string. Used to include the request body fingerprint in the signing payload.
[[nodiscard]] std::string generate_nonce(size_t length = 26uz);Generates a numeric string of length digits using a thread-local std::mt19937_64 seeded from std::random_device. Thread-local storage ensures that concurrent threads do not contend on a shared PRNG state.
Header: <utilities/openapi.hpp>
[[nodiscard]] std::string generate_signature(
CURL* curl,
std::string_view app_key,
std::string_view app_secret,
std::string_view nonce,
std::string_view timestamp,
std::string_view host,
std::string_view request_path,
const std::vector<std::pair<std::string, std::string>>& query_params,
std::string_view request_body);Constructs the canonical signing string and returns a Base64-encoded HMAC-SHA1 signature. A CURL handle is required solely to call curl_easy_escape for percent-encoding. The signing procedure is described in Section 5.4.
Header: <utilities/time.hpp>
[[nodiscard]] std::string get_utc_timestamp();Returns the current UTC time formatted as YYYY-MM-DDTHH:MM:SSZ using std::chrono::utc_clock and std::format. The timestamp is truncated to second precision via std::chrono::floor<std::chrono::seconds>.
Header: <utilities/json.hpp>
[[nodiscard]] nlohmann::json read(const std::filesystem::path& input_path);Opens and parses the JSON file at input_path. Returns an empty nlohmann::json object on failure (file not found, stream error, or parse error) and logs a structured error message. Does not throw.
void write(const nlohmann::json& json, const std::filesystem::path& output_path);Serializes json to output_path with 4-space indentation. Creates parent directories as needed. Logs errors and does not throw.
[[nodiscard]] std::string get_string_from_json(const nlohmann::json& json_obj, const char* key);Returns the string value of key in json_obj, or an empty string if the key is absent or its value is not a string. Logs at debug level on miss.
[[nodiscard]] size_t get_size_t_from_json(const nlohmann::json& json_obj, const char* key);Returns the size_t value of key. Handles integer values directly, and string values via std::from_chars. Returns 0 on absent, null, or unconvertible values.
Header: <client/market.hpp>
Provides access to real-time and historical market data endpoints. Each operation has a synchronous and an asynchronous variant.
class MarketClient {
public:
MarketClient(
wdk::core::CurlPool& pool,
wdk::core::ThreadPool& thread_pool,
const wdk::core::Credentials& credentials,
std::string_view host = "",
std::string_view token = "");
};MarketClient is non-copyable. It stores non-owning references to pool, thread_pool, and credentials; these objects must outlive the client.
Defined as a nested type within MarketClient:
struct MarketRequest {
std::string symbol { "" };
std::string symbols { "" };
std::string category { "" };
std::string timespan { "" };
std::optional<size_t> count { std::nullopt };
std::optional<bool> real_time_required { std::nullopt };
std::string trading_sessions { "" };
std::optional<uint8_t> depth { std::nullopt };
std::optional<bool> extended_hour_required { std::nullopt };
std::optional<bool> overnight_required { std::nullopt };
};All fields default to empty or std::nullopt. Only the fields relevant to a specific endpoint need to be populated; optional fields are omitted from the query string if not set.
MarketRequest Field Reference
| Field | Type | Description |
|---|---|---|
symbol |
string |
Ticker symbol for single-symbol endpoints, e.g., "AAPL". Used by fetch_tick_data, fetch_quotes_data, fetch_historical_bars_data, and fetch_footprint_data |
symbols |
string |
Comma-separated list of ticker symbols for multi-symbol endpoints, e.g., "AAPL,NVDA,MSFT". Used by fetch_snapshot_data and fetch_historical_batch_bars_data |
category |
string |
Instrument category. Use "US_STOCK" for US equities |
timespan |
string |
Bar interval for bar and footprint endpoints. Valid values: "M1", "M5", "M15", "M30", "H", "D" |
count |
optional<size_t> |
Maximum number of records to return. Omitted from the request if not set |
real_time_required |
optional<bool> |
When true, the response includes the latest incomplete (real-time) bar in addition to closed bars. Applies to bar and footprint endpoints |
trading_sessions |
string |
Session filter. Valid values: "PRE", "RTH", "ATH", "OVN" |
depth |
optional<uint8_t> |
Number of order book levels to return. Applies exclusively to fetch_quotes_data |
extended_hour_required |
optional<bool> |
When true, the snapshot response includes extended-hours price and volume fields. Applies to fetch_snapshot_data |
overnight_required |
optional<bool> |
When true, the response includes overnight session (ovn_*) fields. Applies to fetch_snapshot_data and fetch_quotes_data |
symbol and symbols are mutually exclusive by convention: single-symbol endpoints ignore symbols, and multi-symbol endpoints ignore symbol. Populate only the field appropriate to the target method. Optional fields left at std::nullopt are not appended to the query string.
| Method | Endpoint | HTTP | Description |
|---|---|---|---|
fetch_tick_data |
/openapi/market-data/stock/tick |
GET | Recent tick trades for a single symbol |
fetch_snapshot_data |
/openapi/market-data/stock/snapshot |
GET | Real-time price snapshot for one or more symbols |
fetch_quotes_data |
/openapi/market-data/stock/quotes |
GET | Level 1/2 order book (bid/ask) for a single symbol |
fetch_footprint_data |
/openapi/market-data/stock/footprint |
GET | Volume footprint bars (buy/sell breakdown per price level) |
fetch_historical_bars_data |
/openapi/market-data/stock/bars |
GET | OHLCV bars for a single symbol |
fetch_historical_batch_bars_data |
/openapi/market-data/stock/batch-bars |
POST | OHLCV bars for multiple symbols (comma-separated) |
Each method has a corresponding *_async variant that returns std::future<wdk::utilities::Response>.
symbol vs symbols
- Methods operating on a single symbol (
fetch_tick_data,fetch_quotes_data,fetch_historical_bars_data,fetch_footprint_data) useMarketRequest::symbol. - Methods that accept multiple symbols (
fetch_snapshot_data,fetch_historical_batch_bars_data) useMarketRequest::symbolsas a comma-separated list (e.g.,"AAPL,NVDA").
Timespan Values (for bar/footprint endpoints)
| Value | Description |
|---|---|
"M1" |
1-minute bars |
"M5" |
5-minute bars |
"M15" |
15-minute bars |
"M30" |
30-minute bars |
"H1" |
1-hour bars |
"D1" |
Daily bars |
Trading Session Values
| Value | Description |
|---|---|
"PRE" |
Pre-market session |
"CORE" |
Regular trading session |
"POST" |
After-hours session |
"ALL_DAY" |
All sessions combined |
Header: <client/trading.hpp>
Provides order management, account, and instrument query operations.
class TradingClient {
public:
TradingClient(
wdk::core::CurlPool& pool,
wdk::core::ThreadPool& thread_pool,
const wdk::core::Credentials& credentials,
std::string_view host,
std::string_view token);
};struct OrderRequest {
std::string account_id { "" };
std::string combo_type { "" };
std::string client_order_id { "" };
std::string instrument_type { "" };
std::string market { "" };
std::string symbol { "" };
std::string order_type { "" };
std::string entrust_type { "" };
std::string trading_session { "" };
std::string time_in_force { "" };
std::string side { "" };
std::optional<double> quantity { std::nullopt };
std::optional<double> limit_price { std::nullopt };
std::optional<double> stop_price { std::nullopt };
};OrderRequest Field Reference
| Field | Type | Description |
|---|---|---|
account_id |
string |
Brokerage account identifier. Obtain via get_account_id() |
combo_type |
string |
Order combo type. Use "NORMAL" for single-leg orders |
client_order_id |
string |
Client-assigned unique order identifier (nonce). Generate via wdk::utilities::generate_nonce() |
instrument_type |
string |
Asset class. Use "EQUITY" for stocks |
market |
string |
Market identifier, e.g., "US" |
symbol |
string |
Ticker symbol, e.g., "AAPL", "NVDA" |
order_type |
string |
"LIMIT", "MARKET", "STOP", "STOP_LIMIT" |
entrust_type |
string |
Entrustment type. Use "QTY" for quantity-based orders |
trading_session |
string |
"PRE", "RTH", "ATH", "OVN" |
time_in_force |
string |
"DAY", "GTC", "IOC", "FOK" |
side |
string |
"BUY" or "SELL" |
quantity |
optional<double> |
Number of shares |
limit_price |
optional<double> |
Limit price. Required for LIMIT and STOP_LIMIT orders |
stop_price |
optional<double> |
Stop price. Required for STOP and STOP_LIMIT orders |
Numeric fields quantity, limit_price, and stop_price are serialized as strings in the JSON payload: quantity with default precision, limit_price and stop_price with exactly two decimal places.
struct QueryRequest {
std::string symbols { "" };
std::string category { "" };
std::string status { "" };
std::string last_instrument_id { "" };
std::string account_id { "" };
std::string start_date { "" };
std::optional<size_t> page_size { std::nullopt };
std::string last_client_id { "" };
std::string client_order_id { "" };
};| Method | Endpoint | HTTP | Description |
|---|---|---|---|
preview_order |
/openapi/trade/order/preview |
POST | Simulate an order and receive estimated costs without placing it |
place_order |
/openapi/trade/order/place |
POST | Submit an order for execution |
modify_order |
/openapi/trade/order/replace |
POST | Modify the quantity, price, or time-in-force of an open order |
cancel_order |
/openapi/trade/order/cancel |
POST | Cancel an open order by its client_order_id |
Each method has a corresponding *_async variant.
modify_order accepts only the mutable fields: account_id, client_order_id, quantity, limit_price, stop_price, and time_in_force. Only fields that are set are included in the modification payload.
cancel_order requires only account_id and client_order_id.
| Method | Endpoint | HTTP | Description |
|---|---|---|---|
fetch_stock_instrument |
/openapi/instrument/stock/list |
GET | Retrieve instrument metadata for one or more symbols |
fetch_order_history |
/openapi/trade/order/history |
GET | Retrieve historical orders for an account |
fetch_open_order |
/openapi/trade/order/open |
GET | Retrieve currently open orders for an account |
fetch_order_detail |
/openapi/trade/order/detail |
GET | Retrieve the details of a specific order by client_order_id |
| Method | Endpoint | HTTP | Description |
|---|---|---|---|
fetch_account_list |
/openapi/account/list |
GET | Retrieve all brokerage accounts associated with the token |
fetch_account_balance |
/openapi/assets/balance |
GET | Retrieve cash and buying power for an account |
fetch_account_position |
/openapi/assets/positions |
GET | Retrieve open positions for an account |
get_account_id() |
— | — | Convenience method: calls fetch_account_list, parses the first account ID, caches it, and returns it |
get_account_id() caches the result internally. The first call issues a network request; subsequent calls return the cached value synchronously.
Header: <data/data.hpp>
Represents the most recent trade tick for a single symbol.
| Field | Type | Description |
|---|---|---|
symbol |
string |
Ticker symbol |
instrument_id |
string |
Internal instrument identifier |
volume |
string |
Trade volume |
side |
string |
Trade side ("BUY" / "SELL") |
trading_sessions |
string |
Session in which the tick occurred |
Represents a full market snapshot for a single instrument, including regular, extended-hours, and overnight session data.
Key fields include: symbol, instrument_id, price, open, close, high, low, volume, change, change_ratio, pre_close, last_trade_time, ask, ask_size, bid, bid_size, extended-hour OHLCV and bid/ask fields, and overnight session (ovn_*) equivalents.
One level of the order book.
| Field | Type | Description |
|---|---|---|
price |
string |
Price at this level |
size |
string |
Aggregate size at this price |
The full order book for a single symbol.
| Field | Type | Description |
|---|---|---|
symbol |
string |
Ticker symbol |
instrument_id |
string |
Internal instrument identifier |
quote_time |
size_t |
Unix timestamp (ms) of the quote |
asks |
vector<QuoteLevel> |
Ask side, best price first |
bids |
vector<QuoteLevel> |
Bid side, best price first |
One OHLCV footprint bar with per-price-level buy/sell volume breakdown.
| Field | Type | Description |
|---|---|---|
time |
string |
Bar open time |
trading_session |
string |
Session identifier |
total |
string |
Total volume |
delta |
string |
Buy volume minus sell volume |
buy_total |
string |
Aggregate buy volume |
sell_total |
string |
Aggregate sell volume |
buy_detail |
map<string, string> |
Buy volume keyed by price level |
sell_detail |
map<string, string> |
Sell volume keyed by price level |
Collection of FootPrintBar objects for one symbol.
One OHLCV bar.
| Field | Type | Description |
|---|---|---|
time |
string |
Bar open time |
open |
string |
Opening price |
high |
string |
High price |
low |
string |
Low price |
close |
string |
Closing price |
volume |
string |
Volume |
trading_session |
string |
Session identifier |
Collection of Bar objects for one symbol.
All conversion functions accept a wdk::utilities::Response by value, parse response.message with nlohmann::json::parse(..., nullptr, false) (no-throw mode), and return a default-constructed result on parse failure.
| Function | Input | Return Type | Description |
|---|---|---|---|
convert_response_to_tick_data |
Response |
TickData |
Extracts the first tick from result[] array |
convert_response_to_snapshot_data |
Response |
SnapshotData |
Handles multiple JSON schemas (array root, result[], data[], bare object) |
convert_response_to_snapshot_vector |
Response |
vector<SnapshotData> |
Batch snapshot conversion supporting the same schema variants |
convert_response_to_quotes_data |
Response |
QuotesData |
Parses asks[] and bids[] arrays into vector<QuoteLevel> |
convert_response_to_footprint_vector |
Response |
vector<FootPrintData> |
Parses footprint bars including buy_detail and sell_detail maps |
convert_response_to_historical_bars_vector |
Response |
vector<HistoricalBarsData> |
Handles both single-symbol array and multi-symbol result[] schemas |
All monetary and volume values are returned as
std::stringexactly as received from the API. This preserves full precision and avoids floating-point representation issues. Callers requiring numeric operations should use an appropriate decimal arithmetic library.
The following objects must be constructed before any client can be used. They are typically held by the application's top-level class.
#include <core/credentials.hpp>
#include <core/curl_pool.hpp>
#include <core/thread_pool.hpp>
#include <core/token.hpp>
static constexpr std::string_view HOST { "api.webull.co.th" };
static constexpr std::string_view TOKEN_PATH { "examples/res/token.json" };
static constexpr std::string_view CREDENTIALS_PATH { "examples/res/credentials.json" };
// Allocate infrastructure (order matters: pool and credentials before token)
auto curl_pool = std::make_unique<wdk::core::CurlPool>(10uz);
auto thread_pool = std::make_unique<wdk::core::ThreadPool>();
auto credentials = std::make_unique<wdk::core::Credentials>(CREDENTIALS_PATH);
// Token constructor blocks until the session is active.
// The user may need to approve the login in the Webull mobile app.
auto token = std::make_unique<wdk::core::Token>(
TOKEN_PATH, *curl_pool, *credentials, HOST
);wdk::client::MarketClient market_client(
*curl_pool, *thread_pool, *credentials, HOST, token->get_handle()
);
std::future<wdk::utilities::Response> future = market_client.fetch_tick_data_async({
.symbol { "AAPL" },
.category { "US_STOCK" },
.count { 2uz },
.trading_sessions { "PRE" }
});
wdk::utilities::Response response = future.get();
if (response.http_code == 200L) {
wdk::data::TickData tick = wdk::data::convert_response_to_tick_data(response);
// Access tick.symbol, tick.volume, tick.side, etc.
}std::future<wdk::utilities::Response> future = market_client.fetch_snapshot_data_async({
.symbols { "AAPL,NVDA,MSFT" },
.category { "US_STOCK" },
.extended_hour_required { false },
.overnight_required { false }
});
wdk::utilities::Response response = future.get();
if (response.http_code == 200L) {
std::vector<wdk::data::SnapshotData> snapshots =
wdk::data::convert_response_to_snapshot_vector(response);
for (const auto& snap : snapshots) {
// Access snap.symbol, snap.price, snap.change_ratio, etc.
}
}std::future<wdk::utilities::Response> future = market_client.fetch_quotes_data_async({
.symbol { "AAPL" },
.category { "US_STOCK" },
.depth { 5u },
.overnight_required { false }
});
wdk::utilities::Response response = future.get();
if (response.http_code == 200L) {
wdk::data::QuotesData quotes = wdk::data::convert_response_to_quotes_data(response);
// Access quotes.asks and quotes.bids (vectors of QuoteLevel)
}std::future<wdk::utilities::Response> future = market_client.fetch_historical_bars_data_async({
.symbol { "AAPL" },
.category { "US_STOCK" },
.timespan { "M5" },
.count { 100uz },
.real_time_required { false },
.trading_sessions { "CORE" }
});
wdk::utilities::Response response = future.get();
if (response.http_code == 200L) {
std::vector<wdk::data::HistoricalBarsData> history =
wdk::data::convert_response_to_historical_bars_vector(response);
for (const auto& symbol_data : history) {
for (const auto& bar : symbol_data.bars) {
// Access bar.time, bar.open, bar.high, bar.low, bar.close, bar.volume
}
}
}The batch endpoint uses a POST request with a JSON body. Pass a comma-separated list to symbols:
std::future<wdk::utilities::Response> future = market_client.fetch_historical_batch_bars_data_async({
.symbols { "AAPL,NVDA,TSLA" },
.category { "US_STOCK" },
.timespan { "D1" },
.count { 30uz },
.real_time_required { false },
.trading_sessions { "CORE" }
});Before placing orders, always call
preview_orderfirst to validate the request and review estimated costs.
wdk::client::TradingClient trading_client(
*curl_pool, *thread_pool, *credentials, HOST, token->get_handle()
);
const std::string account_id = trading_client.get_account_id();
const std::string client_order_id = wdk::utilities::generate_nonce();
// Step 1: Preview the order
wdk::utilities::Response preview = trading_client.preview_order({
.account_id { account_id },
.combo_type { "NORMAL" },
.client_order_id { client_order_id },
.instrument_type { "EQUITY" },
.market { "US" },
.symbol { "NVDA" },
.order_type { "LIMIT" },
.entrust_type { "QTY" },
.trading_session { "CORE" },
.time_in_force { "DAY" },
.side { "BUY" },
.quantity { 1.0 },
.limit_price { 135.00 },
.stop_price { std::nullopt }
});
// Step 2: Place the order (only if preview confirms acceptable terms)
if (preview.http_code == 200L) {
wdk::utilities::Response placed = trading_client.place_order({
.account_id { account_id },
.combo_type { "NORMAL" },
.client_order_id { client_order_id },
.instrument_type { "EQUITY" },
.market { "US" },
.symbol { "NVDA" },
.order_type { "LIMIT" },
.entrust_type { "QTY" },
.trading_session { "CORE" },
.time_in_force { "DAY" },
.side { "BUY" },
.quantity { 1.0 },
.limit_price { 135.00 },
.stop_price { std::nullopt }
});
}
// Step 3: Modify the order price
wdk::utilities::Response modified = trading_client.modify_order({
.account_id { account_id },
.client_order_id { client_order_id },
.time_in_force { "DAY" },
.quantity { 1.0 },
.limit_price { 134.50 },
.stop_price { std::nullopt }
});
// Step 4: Cancel the order
wdk::utilities::Response cancelled = trading_client.cancel_order({
.account_id { account_id },
.client_order_id { client_order_id }
});// Fetch account list (and resolve the primary account ID in one call)
const std::string account_id = trading_client.get_account_id();
// Account balance
std::future<wdk::utilities::Response> balance_future =
trading_client.fetch_account_balance_async(account_id);
wdk::utilities::Response balance = balance_future.get();
// Open positions
std::future<wdk::utilities::Response> position_future =
trading_client.fetch_account_position_async(account_id);
wdk::utilities::Response positions = position_future.get();
// Order history (paginated)
std::future<wdk::utilities::Response> history_future =
trading_client.fetch_order_history_async({
.account_id { account_id },
.start_date { "2026-01-01" },
.page_size { 50uz },
.last_client_id { "" }
});
wdk::utilities::Response history = history_future.get();| Code | Meaning | SDK Behavior |
|---|---|---|
200 |
Success | Return Response with body |
400 |
Bad Request | Return Response; error body is logged |
401 |
Unauthorized | Return Response; check token validity and credentials |
403 |
Forbidden | Return Response; check account permissions |
404 |
Not Found | Return Response |
429 |
Rate Limited | Automatic exponential backoff; retry up to 3 times (500ms / 1000ms / 2000ms) |
500+ |
Server Error | Return Response; error body is logged |
After 3 failed retries due to rate limiting, execute_request returns a synthetic Response with http_code = 429 and a JSON error body.
All client methods return wdk::utilities::Response. The canonical pattern for consuming a response is:
wdk::utilities::Response response = /* ... */;
if (response.http_code == 200L) {
// Parse response.message as JSON or pass to a converter function
auto json = nlohmann::json::parse(response.message);
// ...
} else {
spdlog::error("Request failed with HTTP {}: {}", response.http_code, response.message);
}If curl_easy_perform returns a non-CURLE_OK code, http_code is set to the CURLcode value and message contains a JSON body of the form {"error": "curl is nullptr"} or equivalent. These values are always less than 100, which distinguishes them from valid HTTP status codes.
| Constructor | Exception | Condition |
|---|---|---|
Credentials |
std::runtime_error |
JSON file cannot be parsed or fields are missing |
Token |
std::runtime_error |
Token fails to reach "NORMAL" status |
ThreadPool::enqueue |
std::runtime_error |
Enqueue called after pool destruction |
WDK ships with CMakePresets.json defining the following presets:
| Preset Name | Display Name | Generator | Build Type | Use Case |
|---|---|---|---|---|
debug |
GCC Debug (Make) | Make | Debug | Compatibility fallback |
debug-ninja |
GCC Debug (Ninja) | Ninja | Debug | Recommended for development |
release |
GCC Release (Ninja) | Ninja | Release | Production distribution |
All presets inherit from a base preset that sets CC=gcc, CXX=g++, and enables CMAKE_EXPORT_COMPILE_COMMANDS=ON for tooling integration (e.g., clangd).
Build output is placed under out/build/<preset-name>/. The install prefix is out/install/<preset-name>/.
build.sh is a convenience wrapper around CMake. It performs the following in order:
- Validates that
cmakeandninjaare onPATH. - Optionally removes the build directory (on
--clean). - Runs
cmake -B <build-dir> -G Ninja -DCMAKE_BUILD_TYPE=<type>. - Runs
cmake --build <build-dir> --parallel <nproc>.
The script uses strict mode (set -euo pipefail) and installs a trap to report the exit code on failure.
run.sh locates the demo binary at examples/bin/Webull-SDK-Demo, checks that it exists and is executable, launches it with any arguments forwarded via "$@", and reports the exit code. It does not re-build; call build.sh first.
CMAKE_EXPORT_COMPILE_COMMANDS=ON is set in all presets. This generates out/build/<preset>/compile_commands.json, which can be symlinked to the project root for use with clangd or other LSP servers:
ln -sf out/build/debug-ninja/compile_commands.json compile_commands.jsonWDK is released under the MIT License.
MIT License
Copyright (c) 2025 Pooh555
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The full license text is available at: https://github.com/Pooh555/Webull-SDK/blob/main/LICENSE
WDK — Webull Developer Kit
Authored by Pooh555