Documentation
¶
Overview ¶
Package webull provides a Go client for the Webull OpenAPI.
This is an independent open-source project and is not affiliated with, maintained by, or endorsed by Webull.
Construct a Client with NewClient and use its service clients:
client, err := webull.NewClient(webull.Config{
AppKey: os.Getenv("WEBULL_APP_KEY"),
AppSecret: os.Getenv("WEBULL_APP_SECRET"),
Environment: webull.Sandbox,
})
accounts, err := client.Trade.Accounts(ctx)
There is no default Environment: sandbox and production are different deployments with separately issued credentials, and a sandbox key returns "404 Route Not Found" for every production path.
Every request is signed automatically. Failures from Webull are returned as *APIError, which matches the package's sentinel errors with errors.Is. The client never retries a POST, because in this API a replayed order is a duplicated order.
The package is under active development and its public API is not yet stable.
Example ¶
Example shows the entry point: construct a client, then reach a service through it. Credentials come from the Webull developer portal; sandbox and production keys are separate.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/sfreiberg/webull"
)
func main() {
client, err := webull.NewClient(webull.Config{
AppKey: os.Getenv("WEBULL_APP_KEY"),
AppSecret: os.Getenv("WEBULL_APP_SECRET"),
Environment: webull.Sandbox, // or webull.Production; there is no default
})
if err != nil {
log.Fatal(err)
}
accounts, err := client.Trade.Accounts(context.Background())
if err != nil {
log.Fatal(err)
}
for _, a := range accounts {
fmt.Println(a.AccountID, a.AccountClass)
}
}
Output:
Index ¶
Examples ¶
Constants ¶
const DefaultTimeout = 30 * time.Second
DefaultTimeout applies to requests when Config.HTTPClient is nil. It is restated here rather than referencing transport.DefaultTimeout so godoc shows the value; a test asserts the two stay equal.
const Version = "0.1.0"
Version is the semantic version of this SDK. It is reported in the User-Agent header of outgoing requests so that Webull can identify SDK traffic and so that bug reports can be tied to a specific release.
Development builds between releases carry a -dev suffix.
Variables ¶
var ( // ErrAuthentication indicates the request was not authenticated: a missing, // malformed or rejected signature, or absent credentials. ErrAuthentication = errors.New("webull: authentication failed") // ErrPermission indicates valid credentials that lack access to the // requested resource, such as an account the key cannot see or a market // data tier the key is not entitled to. A market data entitlement failure // also matches marketdata.ErrNotSubscribed, which is more specific. ErrPermission = errors.New("webull: permission denied") // ErrInvalidRequest indicates the request was rejected as malformed or // carrying invalid parameters. ErrInvalidRequest = errors.New("webull: invalid request") // ErrNotFound indicates the endpoint or resource does not exist. ErrNotFound = errors.New("webull: not found") // ErrRateLimited indicates the request exceeded a rate limit. ErrRateLimited = errors.New("webull: rate limited") // ErrServer indicates Webull reported a server-side failure. ErrServer = errors.New("webull: server error") )
Sentinel errors for conditions callers commonly branch on. Match them with errors.Is; the underlying *APIError remains reachable with errors.As.
var ErrMissingCredentials = errors.New("webull: AppKey and AppSecret are required")
ErrMissingCredentials is returned when AppKey or AppSecret is empty.
var ErrRedirectNotAllowed = transport.ErrRedirectNotAllowed
ErrRedirectNotAllowed is returned when Webull responds with a redirect. Redirects are refused because Go forwards the signature headers — including the app key — to the redirect target; see transport.NewHTTPClient.
Functions ¶
Types ¶
type APIError ¶
type APIError struct {
// StatusCode is the HTTP status.
StatusCode int
// Code is Webull's machine-readable error code, such as
// "OPENAPI_PARAM_ERR". Empty for gateway-level errors.
Code string
// Message is Webull's human-readable description.
Message string
// RequestID correlates the request with Webull support, when the response
// carries one.
RequestID string
// RetryAfter is the delay Webull asked the caller to wait, taken from the
// Retry-After header. Zero when the response did not carry one.
//
// The SDK does not retry rate-limited requests automatically: how long to
// wait, and whether waiting is appropriate at all, is the caller's
// decision. This value is what they need to make it.
RetryAfter time.Duration
// Body is the raw response body, bounded in size. It is retained because
// Webull's error catalogue is not exhaustively documented, and the raw
// payload is often the only way to diagnose an unfamiliar failure.
Body []byte
// Truncated reports that Body was cut short at the size limit, so an empty
// Code or Message may reflect unparseable truncated JSON rather than an
// error shape the SDK does not recognise.
Truncated bool
}
APIError is a structured error response from Webull.
Webull returns two different error shapes. Application errors carry a machine-readable code and a message; errors produced by the API gateway, such as an unrouted path, carry only a message under a different key. Both decode into this type, and Code is empty for the latter.
Example ¶
ExampleAPIError shows matching a failure against the sentinel errors. Every failure from Webull is an *APIError carrying the HTTP status, Webull's code and message, and a request ID for support.
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"github.com/sfreiberg/webull"
)
func main() {
client, err := webull.NewClient(webull.Config{
AppKey: os.Getenv("WEBULL_APP_KEY"),
AppSecret: os.Getenv("WEBULL_APP_SECRET"),
Environment: webull.Sandbox,
})
if err != nil {
log.Fatal(err)
}
_, err = client.Trade.Accounts(context.Background())
if errors.Is(err, webull.ErrRateLimited) {
var apiErr *webull.APIError
if errors.As(err, &apiErr) {
fmt.Printf("rate limited; retry after %s\n", apiErr.RetryAfter)
}
}
}
Output:
func (*APIError) ErrorCode ¶
ErrorCode returns Webull's machine-readable code, or "" if none. It lets packages that cannot import this one classify errors by code.
func (*APIError) Is ¶
Is reports whether this error matches one of the package sentinels, so that errors.Is(err, ErrPermission) works without callers inspecting status codes.
func (*APIError) Temporary ¶
Temporary reports whether retrying the same request might succeed.
It is deliberately conservative: it never reports true for a client error, because replaying a rejected request produces the same rejection. Callers must not treat a true result as licence to retry a non-idempotent request such as order placement without first reconciling.
type AccessToken ¶
type AccessToken struct {
// Token is the credential itself, sent as the x-access-token header by
// a Client whose Config.AccessToken is set.
Token string `json:"token"`
// ExpiresAt is when the token becomes invalid.
ExpiresAt time.Time `json:"-"`
// Status is the token's lifecycle state. A newly created token starts
// PENDING until verified through SMS and the Webull app; the test
// environment skips verification.
Status TokenStatus `json:"status"`
}
AccessToken is a token for deployments that require token authentication in addition to the request signature — TokenCheckEnabled reports whether this deployment does. A token lives 15 days by default and is not refreshable; create a new one when it expires.
func (*AccessToken) UnmarshalJSON ¶
func (t *AccessToken) UnmarshalJSON(data []byte) error
UnmarshalJSON decodes the wire shape, whose expiry is epoch milliseconds that Webull may send as a number or a numeric string; the shared wire decoder absorbs both.
type Client ¶
type Client struct {
// Trade covers accounts, balances, positions, activities, instrument
// reference data and orders.
Trade *trade.Client
// MarketData covers snapshots, quotes, ticks, bars and reference data.
MarketData *marketdata.Client
// Events streams real-time trade events over gRPC. Each Subscribe call
// opens its own connection whose lifetime belongs to the caller.
Events *events.Client
// Streaming delivers real-time market data over MQTT. Each Connect call
// opens its own connection whose lifetime belongs to the caller.
Streaming *streaming.Client
// contains filtered or unexported fields
}
Client is the entry point to the Webull OpenAPI.
Construct one with NewClient and share it: it is safe for concurrent use by multiple goroutines, and reusing it reuses the underlying HTTP connection pool. Constructing a Client performs no I/O.
Long-lived streaming connections are deliberately not owned by Client. They are created explicitly so that their lifetime, and the goroutines they run, belong to the caller.
func NewClient ¶
NewClient returns a Client for the given configuration.
It returns an error if credentials are missing or the environment is not one of Sandbox or Production. Errors never contain credential material.
Example ¶
ExampleNewClient supplies a custom User-Agent identifying the calling application, which is appended to the SDK's own.
package main
import (
"log"
"os"
"github.com/sfreiberg/webull"
)
func main() {
client, err := webull.NewClient(webull.Config{
AppKey: os.Getenv("WEBULL_APP_KEY"),
AppSecret: os.Getenv("WEBULL_APP_SECRET"),
Environment: webull.Sandbox,
UserAgent: "acme-trader/1.4",
})
if err != nil {
log.Fatal(err)
}
_ = client
}
Output:
func (*Client) CheckAccessToken ¶
CheckAccessToken reports a token's current status and expiry.
func (*Client) CreateAccessToken ¶
func (c *Client) CreateAccessToken(ctx context.Context) (*AccessToken, error)
CreateAccessToken creates an access token for this app key. The token starts PENDING and must be verified through SMS and the Webull app before it reads NORMAL; the test environment skips verification. Pass the token to a new Client through Config.AccessToken.
Example ¶
ExampleClient_CreateAccessToken shows the token flow for deployments where TokenCheckEnabled reports that token authentication is required.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/sfreiberg/webull"
)
func main() {
client, err := webull.NewClient(webull.Config{
AppKey: os.Getenv("WEBULL_APP_KEY"),
AppSecret: os.Getenv("WEBULL_APP_SECRET"),
Environment: webull.Sandbox,
})
if err != nil {
log.Fatal(err)
}
needed, err := client.TokenCheckEnabled(context.Background())
if err != nil {
log.Fatal(err)
}
if !needed {
return // signing alone is enough for this deployment
}
// The token starts PENDING until verified through SMS and the Webull
// app; it lives 15 days and is then recreated, not refreshed.
tok, err := client.CreateAccessToken(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Println(tok.Status, tok.ExpiresAt)
// Requests carry the token once a client is built with it.
authed, err := webull.NewClient(webull.Config{
AppKey: os.Getenv("WEBULL_APP_KEY"),
AppSecret: os.Getenv("WEBULL_APP_SECRET"),
Environment: webull.Sandbox,
AccessToken: tok.Token,
})
if err != nil {
log.Fatal(err)
}
_ = authed
}
Output:
func (*Client) Environment ¶
func (c *Client) Environment() Environment
Environment reports which deployment this client talks to.
func (*Client) TokenCheckEnabled ¶
TokenCheckEnabled reports whether this deployment requires an access token in addition to the request signature.
Webull gates token authentication per deployment rather than documenting it statically, so the answer must be asked for. It is not fetched during NewClient, because constructing a client should not perform I/O.
type Config ¶
type Config struct {
// AppKey and AppSecret are the credentials issued by Webull. They are used
// to sign every request and are never logged or included in errors.
AppKey string
AppSecret string
// Environment selects sandbox or production. There is no default: choosing
// between simulated and real orders is not a decision this SDK will make on
// a caller's behalf.
Environment Environment
// HTTPClient is used for all HTTP requests. If nil, a client with
// DefaultTimeout is used. Supply your own to control transport, proxies or
// connection pooling.
HTTPClient *http.Client
// EndpointOverrides replaces the host for a named service, for testing
// against a local server or routing through a proxy. Keys are service
// names: "trading", "marketdata", "streaming", "events". Each is
// resolved independently: trading and market data share a host by
// default, but overriding one does not override the other. The Connect
// API is configured through connect.Config, which has its own Endpoint
// override.
EndpointOverrides map[string]string
// UserAgent is appended to the SDK's own User-Agent, identifying the
// calling application. Optional.
UserAgent string
// AccessToken is sent as the x-access-token header on every request, for
// deployments where TokenCheckEnabled reports that token authentication
// is required in addition to the signature. Create one with
// Client.CreateAccessToken. Optional; most deployments do not need it.
AccessToken string
}
Config describes how a Client connects to Webull.
The zero value is not usable: AppKey, AppSecret and Environment are required.
type Environment ¶
type Environment string
Environment selects which Webull deployment the SDK talks to.
const ( // Sandbox is Webull's test environment. Orders placed here are simulated. Sandbox Environment = "sandbox" // Production is the live environment. Orders placed here are real. Production Environment = "production" )
func (Environment) IsProduction ¶
func (e Environment) IsProduction() bool
IsProduction reports whether e is the live environment. Guard rails that must never fire against real accounts should test this rather than comparing strings, so that a future environment is not accidentally treated as safe.
func (Environment) Valid ¶
func (e Environment) Valid() bool
Valid reports whether e is a known environment.
type TokenStatus ¶
type TokenStatus string
TokenStatus is the lifecycle state of an access token.
const ( // TokenPending means the token awaits verification through SMS and the // Webull app. The test environment skips verification. TokenPending TokenStatus = "PENDING" // TokenNormal means the token is valid. TokenNormal TokenStatus = "NORMAL" // TokenInvalid means the token is not usable. TokenInvalid TokenStatus = "INVALID" // TokenExpired means the token's lifetime has lapsed; create a new one. TokenExpired TokenStatus = "EXPIRED" )
Access token statuses.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package connect implements Webull's Connect API: OAuth 2.0 access to other people's Webull accounts on their behalf, for platforms that let their users trade a linked Webull account without leaving the platform.
|
Package connect implements Webull's Connect API: OAuth 2.0 access to other people's Webull accounts on their behalf, for platforms that let their users trade a linked Webull account without leaving the platform. |
|
Package events streams real-time trade events from Webull over gRPC: order status changes, position changes and option events.
|
Package events streams real-time trade events from Webull over gRPC: order status changes, position changes and option events. |
|
internal
|
|
|
endpoints
Package endpoints holds host constants that more than one package resolves, so that a host rename cannot drift between their tables.
|
Package endpoints holds host constants that more than one package resolves, so that a host rename cannot drift between their tables. |
|
eventspb
Package eventspb holds the generated protobuf and gRPC bindings for Webull's trade event stream.
|
Package eventspb holds the generated protobuf and gRPC bindings for Webull's trade event stream. |
|
query
Package query builds URL query strings for service packages.
|
Package query builds URL query strings for service packages. |
|
signing
Package signing implements Webull's HMAC request signature.
|
Package signing implements Webull's HMAC request signature. |
|
streampb
Package streampb holds the generated protobuf bindings for Webull's MQTT market-data payloads.
|
Package streampb holds the generated protobuf bindings for Webull's MQTT market-data payloads. |
|
testutil
Package testutil holds helpers shared by this module's tests.
|
Package testutil holds helpers shared by this module's tests. |
|
transport
Package transport carries out signed HTTP requests against Webull and turns responses into typed results or errors.
|
Package transport carries out signed HTTP requests against Webull and turns responses into typed results or errors. |
|
wire
Package wire holds decoding helpers for value forms that recur across Webull's APIs, shared by the service packages.
|
Package wire holds decoding helpers for value forms that recur across Webull's APIs, shared by the service packages. |
|
Package marketdata provides access to Webull's Market Data API over HTTP: snapshots, quotes, ticks, bars and reference data.
|
Package marketdata provides access to Webull's Market Data API over HTTP: snapshots, quotes, ticks, bars and reference data. |
|
Package streaming delivers real-time market data from Webull over MQTT: snapshots, top-of-book and depth quotes, and ticks for stocks and event contracts.
|
Package streaming delivers real-time market data from Webull over MQTT: snapshots, top-of-book and depth quotes, and ticks for stocks and event contracts. |
|
Package trade provides access to Webull's Trading API: accounts, balances, positions, cash activities, reference data for tradable instruments, and orders across every asset class Webull offers.
|
Package trade provides access to Webull's Trading API: accounts, balances, positions, cash activities, reference data for tradable instruments, and orders across every asset class Webull offers. |