Asynchronous · Thread-Safe · Header-Light · Cryptographically Signed
Built on libcurl · nlohmann/json · OpenSSL · spdlog
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.
Visit the full documentation here: Documentation
Visit the documentation for LLMs here: Documentation for AI Agents
| 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 ninjaCloning the main repository.
Clone 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 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()
);
```cpp
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
}
}
}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();Given Python's dubious appeal and the questionable maintenance of the Java SDK, C++ was an inevitability.