Authentication¶
Webull OpenAPI requests use two independent mechanisms: a per-request signature
and an access token. The SDK handles both. client.New stores the AppKey used
for signing; signing headers are built per-request in Client.Do. Client.EnsureToken
creates and activates an access token and installs it as the x-access-token
header on later requests.
Prerequisites
- A Webull account (sandbox or production)
- App key and app secret — see Getting Started
- Go 1.26+
Request signing¶
Every request is signed with HMAC-SHA1 over a percent-encoded canonical
string. The HMAC key is the app secret followed by &, and the signature is
returned as standard Base64. The gRPC events API uses HMAC-SHA256 with a
different canonical string; see Trading events for details.
The canonical string is assembled as follows:
- Build a list of
name=valueentries from the query parameters and the participating headers (x-app-key,x-signature-algorithm,x-signature-version,x-signature-nonce,x-timestamp,host). - Sort the entries by name. For a repeated name, sort its values and join them
with
&inside the single entry. - Join the entries with
&and append them to the request path with&:path&entry1&entry2.... - When there is a body, append
&and the uppercase hexadecimal MD5 of the exact bytes transmitted. A bodyless request has no digest. - Percent-encode the whole string using the RFC 3986 unreserved set (
A-Z,a-z,0-9,-,_,.,~), with uppercase hex digits. This matches Python'surllib.parse.quote(s, safe="")and encodes&,=,/,:,?, and space.
The HTTP method is not part of the canonical string. host is the request host
as hostname[:port], including the port only when it is not the scheme default.
Two normalization rules matter in practice:
- Query values are sorted so the bytes signed are the bytes sent.
- The body hash is computed over compact JSON with HTML escaping disabled (Go's
json.Marshalwould rewrite<,>, and&, changing the digest). The SDK signs the exact transmitted bytes.
A golden signature test pins this behavior so it cannot drift silently.
Headers¶
Signed requests carry the following headers:
| Header | Description |
|---|---|
x-app-key |
Your Webull OpenAPI app key |
x-timestamp |
Current UTC time in ISO-8601 form, 2006-01-02T15:04:05Z |
x-signature-version |
Signature version, 1.0 |
x-signature-algorithm |
Signature algorithm, HMAC-SHA1 |
x-signature-nonce |
Random 128-bit nonce, 32 lowercase hex characters |
host |
Request host; participates in the signature |
x-version |
API version, v2 by default (or v3 where configured) |
x-signature |
Base64 HMAC-SHA1 signature; not itself signed |
Authenticated endpoints additionally require:
| Header | Description |
|---|---|
x-access-token |
Access token obtained from the token endpoint |
The app secret is not a header. It is used only to compute x-signature and
must never be transmitted.
Token lifecycle¶
Tokens are created with POST /openapi/auth/token/create and checked with
POST /openapi/auth/token/check. Tokens are time-sensitive (15 days by
default). The public API is:
| Method | Behavior |
|---|---|
Client.CreateToken(ctx) |
Creates a token; does not touch the cache |
Client.CheckToken(ctx, token) |
Returns the current status of a token |
Client.EnsureToken(ctx) |
Reuses a cached valid token or creates one, waiting for NORMAL when needed, then caches it |
Client.CurrentToken() |
Returns the cached token, or nil |
Client.AccessToken() |
Returns the cached token value, or "" |
Client.SetToken(token) |
Sets or clears the cached token |
Client.EnableTokenInjection() |
Installs a transport wrapper that injects the x-access-token header automatically |
CurrentToken returns a *Token with the following fields:
| Field | Type | Description |
|---|---|---|
Value |
string |
Access token sent as x-access-token |
ExpiresAt |
time.Time |
Expiry; zero means unspecified |
Status |
TokenStatus |
Current lifecycle state (client.TokenStatusPending, TokenStatusNormal, TokenStatusInvalid, TokenStatusExpired) |
Helper methods: Token.Valid() bool (true when non-empty, NORMAL, and not
expired) and Token.Expired() bool (true when past ExpiresAt).
The header name is the constant client.AccessTokenHeader ("x-access-token").
Poll defaults are client.DefaultTokenPollInterval (5s) and
client.DefaultTokenPollTimeout (5min).
| Status | Meaning |
|---|---|
PENDING |
Created, awaiting verification (Webull App code / 2FA) |
NORMAL |
Valid and usable |
INVALID |
Invalid or was never used |
EXPIRED |
Verification was not completed within five minutes; create a new token |
Token lifecycle diagram¶
CreateToken ──> PENDING ──(2FA / auto)──> NORMAL ──(15 days)──> EXPIRED
│ │
│ └──> INVALID (if never used)
└──> EXPIRED (5 min timeout)
- Sandbox:
CreateTokenreturnsNORMALimmediately (no 2FA). - Production:
CreateTokenreturnsPENDING; complete Webull App 2FA within 5 minutes. EnsureTokenpollsCheckTokenuntil the token becomesNORMAL, becomes terminal, or the poll timeout elapses.
Sandbox tokens are issued as NORMAL automatically, with no 2FA step.
Production tokens start PENDING and become NORMAL after verification.
EnsureToken polls CheckToken until the token becomes NORMAL, becomes
terminal, or the poll timeout elapses. The default interval is 5 seconds and the
default timeout is 5 minutes, changeable with Client.SetTokenPollInterval and
Client.SetTokenPollTimeout.
Once a token is cached, Client.Do attaches it as the x-access-token header
after signing. The header is deliberately omitted from the token endpoints
themselves.
For sandbox automation, client.WithAutoToken(true) makes the first non-token
request obtain a token automatically when the environment is the sandbox. In
production the SDK never starts the 2FA flow implicitly; it returns
client.ErrAccessTokenRequired so you can drive EnsureToken explicitly.
Sandbox¶
Point the client at the sandbox host while developing:
Never commit app keys, app secrets, or access tokens. Supply them through
environment variables (WEBULL_APP_KEY, WEBULL_APP_SECRET) or your own secret
manager. Shared sandbox test accounts (no application required) are published at
https://developer.webull.hk/apis/docs/sdk#test-accounts (HK) and
https://developer.webull.com/apis/docs/sdk#test-accounts (US). The token
endpoint allows 10 requests per 30 seconds.
Related¶
- Getting Started — install, credentials, and first call.
- Patterns — client construction and shared conventions.
- Errors — typed errors and retry patterns.
- Streaming — real-time Market Data over MQTT.