Webull's OpenAPI REST surface — internal.
Every call is signed, including the public-looking ones
There is no anonymous path here. /openapi/market-data/crypto/snapshot needs the same
App Key and signature as an order. That is why this venue declares
credential_benefit: :required — the first in the family to do so — and why
get_price/2 takes credentials that other venues' get_price/2 does not need.
A consumer branching on capabilities/0 learns this before it calls; a consumer that
assumed market data is free learns it from a 401.
Bars nest one level down, and assuming otherwise returns nothing
The crypto-bars response is a list of groups, each carrying its rows under
"result":
[%{"instrument_id" => …, "symbol" => …, "result" => [%{"open" => …}, …]}]The adapter this came from once mapped its row decoder over the group objects. Groups
have no "open", "close" or "time", so every field resolved to nil — the call
returned a list of all-nil bars, and the backfill logged an empty result for every
crypto pair. It looked like the venue had no data.
Both shapes are handled: a group with "result" is flattened, and a flat bar decodes
directly, in case the equities path or a future change sends one.
No volume, anywhere
Webull's crypto OpenAPI exposes no trade volume — not on the bars, not on the
snapshot, not on the MQTT stream. volume is nil rather than 0, because zero is a
volume and this venue is not reporting one. capabilities/0 declares
reports_trade_volume: false so a consumer can route volume-dependent work elsewhere
rather than discovering a column of zeroes.
Summary
Functions
OHLC bars for a symbol and canonical timeframe.
Last price for one symbol, from the crypto snapshot endpoint.
Every crypto symbol the venue lists, canonical.
Canonical timeframes this venue serves, shortest first.
Functions
@spec get_historical_prices(String.t(), String.t(), keyword(), map(), keyword()) :: {:ok, [map()]} | {:error, term()} | {:refused, term()}
OHLC bars for a symbol and canonical timeframe.
Bars carry no volume — see the module doc. A bar without a venue timestamp is an error, not a bar stamped with the local clock.
@spec get_price(String.t(), map(), keyword()) :: {:ok, DpExchange.Core.Types.Quote.t()} | {:error, term()} | {:refused, term()}
Last price for one symbol, from the crypto snapshot endpoint.
@spec get_symbols( map(), keyword() ) :: {:ok, [String.t()]} | {:error, term()} | {:refused, term()}
Every crypto symbol the venue lists, canonical.
Measured 2026-08-05 against /openapi/instrument/crypto/list: 342 symbols, every one
quoted in USD.
@spec timeframes() :: [String.t()]
Canonical timeframes this venue serves, shortest first.