Environment
webull-python-sdk-core, webull-python-sdk-mdata, webull-python-sdk-quotes-core — 0.1.18 (latest on PyPI)
- Python 3.13
- OS: Windows 11
grpcio 1.75.1, paho-mqtt 1.6.1
Summary
Using DefaultQuotesClient for market-data streaming (SNAPSHOT/TICK/QUOTE), we have never received a single tick, on any deployment, across multiple debugging sessions. Two separate, stacked issues:
1. QuotesClient's default construction doesn't match your own documented endpoint contract, and even a corrected construction still fails the TLS handshake
Per https://developer.webull.com/apis/docs/market-data-api/data-streaming-api, the two production endpoints are:
data-api.webull.com:1883 — plaintext TCP
wss://data-api.webull.com:8883/mqtt — WebSocket over TLS
But QuotesClient.__init__ (webullsdkquotescore/quotes_client.py) defaults transport="tcp" (not "websockets") regardless of tls_enable, and DefaultQuotesClient passes that same "tcp" default straight through. So the SDK's own out-of-the-box defaults (tls_enable=True, mqtt_post=8883, transport="tcp") open a raw TCP+TLS socket against an endpoint that's documented as WebSocket-only — a straightforward protocol mismatch.
However, correcting for this doesn't fix it. We tested the fully docs-correct construction directly against paho (transport="websockets", host=data-api.webull.com, port=8883, TLS on, path defaults to /mqtt in paho already) and got the identical failure:
SSLCertVerificationError: ('Peer sent no certificates to verify',)
This is a TLS-handshake-level failure (raised from do_handshake()), before any MQTT or WebSocket framing is even exchanged — so it isn't specific to the transport mode. To isolate further, in the same process/moment we also ran:
1. socket.create_connection(('data-api.webull.com', 8883)) + ssl.create_default_context().wrap_socket(...)
-> OK, cert subject *.webull.com
2. Same host:port, but with an SSLContext built to exactly mirror what
Client.tls_set() constructs internally (ssl.SSLContext(ssl.PROTOCOL_TLS),
verify_mode=CERT_REQUIRED, load_default_certs(), no ca_certs override)
-> OK, cert subject *.webull.com
3. paho.mqtt.client.Client(transport="websockets").connect('data-api.webull.com', 8883)
with the SAME effective SSL context (via Client.tls_set())
-> FAIL, SSLCertVerificationError: Peer sent no certificates to verify
(1) and (2) rule out host, port, cert chain, TLS version, and the specific SSLContext construction paho uses as the cause. Only path (3) — going through paho.mqtt.client.Client's actual connect()/reconnect() (which does wrap_socket(..., do_handshake_on_connect=False) on a socket that gets set non-blocking, then a separately-timed sock.do_handshake()) — fails. We were not able to fully pin down why this specific code path behaves differently from a synchronous bare-socket handshake with an equivalent context; it may be a paho-mqtt 1.6.1-specific issue (an old, EOL-adjacent library this SDK pins via the six-vendoring compatibility shim) rather than something on WeBull's side, but since this SDK requires that paho version, it produces a 100% reproducible dead-on-arrival TLS failure for every caller who uses the SDK's client class against the documented TLS endpoint.
Workaround applied: route the MQTT leg to data-api.webull.com:1883 (plaintext TCP, not WSS) instead, leaving the gRPC leg on usquotes-api.webullfintech.com:443. This is NOT what your docs describe as the TLS-secured path, but it's the only configuration under which paho's handshake succeeds at all.
2. Even after the above fix, the gRPC leg used for token refresh intermittently fails to connect at all
Independent of #1, requests over the gRPC channel (GetStreamingTokenRequest, used by _quotes_connect before every MQTT connect attempt) intermittently fail:
grpc.RpcError: StatusCode.UNAVAILABLE
details = "failed to connect to all addresses; last error: UNKNOWN: ipv4:X.X.X.X:443:
connection attempt timed out before receiving SETTINGS frame"
This happens even on a freshly-started process (within 10-30s of a clean boot) and persists for extended periods (observed continuously for 30+ minutes in one instance). During the SAME time window, from the SAME machine:
- A raw
grpc.secure_channel(host, grpc.ssl_channel_credentials()) (no SDK involved) connects and becomes ready in ~0.25s, and successfully completes real unary RPC calls both immediately and after 75s idle.
grpcurl usquotes-api.webullfintech.com:443 list connects cleanly and gets a proper structured gRPC response from the server.
So the network path and the server are demonstrably healthy at the exact same time the SDK's own channel cannot connect. We believe this may relate to GrpcApiClient being a process-wide singleton (webullsdkquotescore/grpc/grpc_client.py) whose real connection setup (Connect(...) + .run()) is guarded by if not hasattr(self, '_connect') and therefore runs exactly once per process — if that one-shot setup fails or degrades, no later construction attempt in the same process can trigger a retry, since the guard is satisfied by a self._connect that was assigned before .run() could raise. We could not fully confirm this without instrumenting the SDK internals further, but the symptom (works for a while, or fails immediately, then never recovers without a full process restart) is consistent with it.
Steps to reproduce
- Construct
DefaultQuotesClient with valid App Key/Secret, tls_enable=True (default), against data-api.webull.com — or even just transport="websockets" directly against paho.mqtt.client.Client with TLS on, hostname data-api.webull.com, port 8883.
- Call
connect() / connect_and_loop_async().
- Observe:
on_log (if wired — it is NOT wired by default; paho's _easy_log is a silent no-op unless the caller sets on_log or calls enable_logger(), so this failure is invisible without extra instrumentation) — or a direct try/except around connect() — shows SSLCertVerificationError: Peer sent no certificates to verify.
- Compare against a bare
socket.create_connection() + ssl.SSLContext(...).wrap_socket() (same host/port, equivalent context) done synchronously in the same process moments before/after — it succeeds every time.
- Separately: even after routing the MQTT leg to the plaintext
1883 workaround, on_refresh_token intermittently raises UNAVAILABLE from the shared gRPC leg (usquotes-api.webullfintech.com:443), blocking _quotes_connect entirely — sometimes immediately on a fresh process, sometimes after several successful MQTT connect/disconnect cycles.
Expected behavior
DefaultQuotesClient/connect_and_loop_async() establishes a working MQTT connection and delivers ticks for subscribed symbols.
Actual behavior
Zero ticks ever delivered. Two independent, stacked connection failures as described above.
Additional context
Happy to share full logs/a minimal repro script if useful — wanted to keep this issue focused on the two concrete failure modes we isolated rather than the full debugging trail. This blocked market-data streaming entirely for our use case; we've worked around it with the port-1883 patch for issue #1, but issue #2 remains unresolved and intermittently blocks even that workaround.
Environment
webull-python-sdk-core,webull-python-sdk-mdata,webull-python-sdk-quotes-core— 0.1.18 (latest on PyPI)grpcio1.75.1,paho-mqtt1.6.1Summary
Using
DefaultQuotesClientfor market-data streaming (SNAPSHOT/TICK/QUOTE), we have never received a single tick, on any deployment, across multiple debugging sessions. Two separate, stacked issues:1.
QuotesClient's default construction doesn't match your own documented endpoint contract, and even a corrected construction still fails the TLS handshakePer https://developer.webull.com/apis/docs/market-data-api/data-streaming-api, the two production endpoints are:
data-api.webull.com:1883— plaintext TCPwss://data-api.webull.com:8883/mqtt— WebSocket over TLSBut
QuotesClient.__init__(webullsdkquotescore/quotes_client.py) defaultstransport="tcp"(not"websockets") regardless oftls_enable, andDefaultQuotesClientpasses that same"tcp"default straight through. So the SDK's own out-of-the-box defaults (tls_enable=True,mqtt_post=8883,transport="tcp") open a raw TCP+TLS socket against an endpoint that's documented as WebSocket-only — a straightforward protocol mismatch.However, correcting for this doesn't fix it. We tested the fully docs-correct construction directly against paho (
transport="websockets", host=data-api.webull.com, port=8883, TLS on, path defaults to/mqttin paho already) and got the identical failure:This is a TLS-handshake-level failure (raised from
do_handshake()), before any MQTT or WebSocket framing is even exchanged — so it isn't specific to the transport mode. To isolate further, in the same process/moment we also ran:(1) and (2) rule out host, port, cert chain, TLS version, and the specific
SSLContextconstruction paho uses as the cause. Only path (3) — going throughpaho.mqtt.client.Client's actualconnect()/reconnect()(which doeswrap_socket(..., do_handshake_on_connect=False)on a socket that gets set non-blocking, then a separately-timedsock.do_handshake()) — fails. We were not able to fully pin down why this specific code path behaves differently from a synchronous bare-socket handshake with an equivalent context; it may be a paho-mqtt 1.6.1-specific issue (an old, EOL-adjacent library this SDK pins via thesix-vendoring compatibility shim) rather than something on WeBull's side, but since this SDK requires that paho version, it produces a 100% reproducible dead-on-arrival TLS failure for every caller who uses the SDK's client class against the documented TLS endpoint.Workaround applied: route the MQTT leg to
data-api.webull.com:1883(plaintext TCP, not WSS) instead, leaving the gRPC leg onusquotes-api.webullfintech.com:443. This is NOT what your docs describe as the TLS-secured path, but it's the only configuration under which paho's handshake succeeds at all.2. Even after the above fix, the gRPC leg used for token refresh intermittently fails to connect at all
Independent of #1, requests over the gRPC channel (
GetStreamingTokenRequest, used by_quotes_connectbefore every MQTT connect attempt) intermittently fail:This happens even on a freshly-started process (within 10-30s of a clean boot) and persists for extended periods (observed continuously for 30+ minutes in one instance). During the SAME time window, from the SAME machine:
grpc.secure_channel(host, grpc.ssl_channel_credentials())(no SDK involved) connects and becomes ready in ~0.25s, and successfully completes real unary RPC calls both immediately and after 75s idle.grpcurl usquotes-api.webullfintech.com:443 listconnects cleanly and gets a proper structured gRPC response from the server.So the network path and the server are demonstrably healthy at the exact same time the SDK's own channel cannot connect. We believe this may relate to
GrpcApiClientbeing a process-wide singleton (webullsdkquotescore/grpc/grpc_client.py) whose real connection setup (Connect(...)+.run()) is guarded byif not hasattr(self, '_connect')and therefore runs exactly once per process — if that one-shot setup fails or degrades, no later construction attempt in the same process can trigger a retry, since the guard is satisfied by aself._connectthat was assigned before.run()could raise. We could not fully confirm this without instrumenting the SDK internals further, but the symptom (works for a while, or fails immediately, then never recovers without a full process restart) is consistent with it.Steps to reproduce
DefaultQuotesClientwith valid App Key/Secret,tls_enable=True(default), againstdata-api.webull.com— or even justtransport="websockets"directly againstpaho.mqtt.client.Clientwith TLS on, hostnamedata-api.webull.com, port8883.connect()/connect_and_loop_async().on_log(if wired — it is NOT wired by default;paho's_easy_logis a silent no-op unless the caller setson_logor callsenable_logger(), so this failure is invisible without extra instrumentation) — or a directtry/exceptaroundconnect()— showsSSLCertVerificationError: Peer sent no certificates to verify.socket.create_connection()+ssl.SSLContext(...).wrap_socket()(same host/port, equivalent context) done synchronously in the same process moments before/after — it succeeds every time.1883workaround,on_refresh_tokenintermittently raisesUNAVAILABLEfrom the shared gRPC leg (usquotes-api.webullfintech.com:443), blocking_quotes_connectentirely — sometimes immediately on a fresh process, sometimes after several successful MQTT connect/disconnect cycles.Expected behavior
DefaultQuotesClient/connect_and_loop_async()establishes a working MQTT connection and delivers ticks for subscribed symbols.Actual behavior
Zero ticks ever delivered. Two independent, stacked connection failures as described above.
Additional context
Happy to share full logs/a minimal repro script if useful — wanted to keep this issue focused on the two concrete failure modes we isolated rather than the full debugging trail. This blocked market-data streaming entirely for our use case; we've worked around it with the port-1883 patch for issue #1, but issue #2 remains unresolved and intermittently blocks even that workaround.