bitget-go
Go client for Bitget UTA API v3 with typed models, string-based prices, auto-reconnecting WebSocket, and demo trading
About bitget-go
This large README is shown as a plain-text preview. Read the full README at the source
# Bitget Golang Client/SDK/Library

[](https://github.com/tigusigalpa/bitget-go/actions/workflows/ci.yml)
[](https://github.com/tigusigalpa/bitget-go/actions/workflows/test.yml)
[](https://golang.org/)
[](LICENSE)
[](https://github.com/tigusigalpa/bitget-go/actions/workflows/codeql.yml)
[](https://codecov.io/gh/tigusigalpa/bitget-go)
[](https://github.com/tigusigalpa/bitget-go/releases)
[](https://pkg.go.dev/github.com/tigusigalpa/bitget-go)
An intentionally small, typed Go client for the [Bitget Unified Trading Account (UTA) API v3](https://www.bitget.com/docs/uta/quick-start). It takes care of request signing, response envelopes, WebSocket lifecycle, and numeric precision, while leaving trading decisions entirely in your application.
It is not an official Bitget SDK and it does not execute a request during import or client creation.
## What this library is for
Think of the SDK as a small transport layer between your Go application and
Bitget. It signs private REST requests, turns documented responses into Go
types, and keeps a WebSocket connection alive. It deliberately does **not**
make trading decisions, convert prices to `float64`, or hide reconnects and
subscription errors from your application.
That gives you a simple rule of thumb:
- Use **REST** when you need a snapshot: balances, an order book, a recent
page of orders, candles, or funding history.
- Use the regular **WebSocket subscription channel** when a bounded in-process
feed is enough.
- Use the **raw-frame and lifecycle handlers** when every received payload,
its receipt time, and the current connection generation matter to your
downstream system.
## Start here
Start with a public REST call. It needs no credentials and cannot touch an
account. In a new directory, create a tiny Go module and run the program:
```bash
mkdir bitget-first-request
cd bitget-first-request
go mod init example.com/bitget-first-request
go get github.com/tigusigalpa/bitget-go
# Save the program below as main.go, then:
go run .
```
The module requires Go 1.21 or newer. Prices, quantities, balances, fees, and timestamps are represented as strings where the API returns strings. That avoids accidental `float64` rounding in financial code.
```go
package main
import (
"context"
"fmt"
"log"
bitget "github.com/tigusigalpa/bitget-go"
"github.com/tigusigalpa/bitget-go/models"
)
func main() {
client := bitget.NewRestClient("", "", "") // public endpoints need no key
tickers, err := client.Market.GetTickers(context.Background(), models.CategorySpot, "BTCUSDT")
if err != nil {
log.Fatal(err)
}
if len(tickers) > 0 {
fmt.Printf("BTC/USDT last price: %s\n", tickers[0].LastPrice)
}
}
```
If you prefer to explore the repository examples, clone the project and run
`go run ./examples/rest`. The public REST and public WebSocket examples are
safe to run without environment variables.
## Choose the smallest client for the job
| Need | Create | Credentials | What it does |
|---|---|---|---|
| Snapshot market data | `NewRestClient("", "", "")` | No | Public REST calls such as tickers and order book |
| Account, orders, positions | `NewRestClient(key, secret, passphrase)` | Yes | Signed private REST calls |
| Live public market feed | `NewPublicWSClient()` | No | Subscribes to public WebSocket channels |
| Private fills and account feed | `NewPrivateWSClient(key, secret, passphrase)` | Yes | Logs in to a private WebSocket and subscribes to its channels |
There is no automatic switch between public and private APIs, production and demo endpoints, or REST and WebSocket. You choose the transport and credentials explicitly.
### A practical public market query
The market methods return strings for exchange-provided decimals and timestamps.
Keep those values as strings until your own domain layer deliberately converts
them with the precision rules your application needs. This complete helper
needs the `market` package in addition to the imports from the first example.
```go
import (
"context"
"fmt"
"time"
bitget "github.com/tigusigalpa/bitget-go"
"github.com/tigusigalpa/bitget-go/models"
"github.com/tigusigalpa/bitget-go/rest/market"
)
func printRecentMarket() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
client := bitget.NewRestClient("", "", "")
candles, err := client.Market.GetCandles(ctx, market.GetCandlesOptions{
Category: models.CategorySpot,
Symbol: "BTCUSDT",
Interval: "1m",
Limit: "10",
})
if err != nil {
return err
}
for _, candle := range candles {
// candle = [timestamp, open, high, low, close, base volume, turnover]
fmt.Printf("close=%s at %s\n", candle[4], candle[0])
}
fills, err := client.Market.GetPublicFills(ctx, models.CategorySpot, "BTCUSDT", "20")
if err != nil {
return err
}
for _, fill := range fills {
fmt.Printf("%s %s @ %s (exec %s)\n", fill.Side, fill.Size, fill.Price, fill.ExecID)
}
return nil
}
```
`GetPublicFills` is a recent-data repair source, not a complete historical
trade ledger. For futures funding data, call `GetFundingRateHistory` with the
same category and symbol; use the corresponding instrument's `FundInterval`
instead of assuming a fixed settlement schedule.
## Demo credentials before production
Create a separate **Demo API key** in Bitget. Keep all three values outside source control:
- `BITGET_API_KEY`
- `BITGET_SECRET_KEY`
- `BITGET_PASSPHRASE`
Bitget requires the `paptrading: 1` header for demo REST requests. `WithDemoTrading()` adds it. The accompanying private examples additionally refuse to run until `BITGET_DEMO=1` is set; this is a guard against accidentally pointing copied code at a production account.
```go
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client := bitget.NewRestClient(
os.Getenv("BITGET_API_KEY"),
os.Getenv("BITGET_SECRET_KEY"),
os.Getenv("BITGET_PASSPHRASE"),
bitget.WithDemoTrading(),
)
assets, err := client.Account.GetAssets(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println("demo USD equity:", assets.AccountEquity)
```
Run the complete safe demo-account example with:
```bash
BITGET_DEMO=1 BITGET_API_KEY=... BITGET_SECRET_KEY=... BITGET_PASSPHRASE=... go run ./examples/demo-account
```
Never put keys, passphrases, signatures, or `.env` files in a repository or issue. Bind an IP address to an API key where possible. See Bitget's [API-key security guidance](https://www.bitget.com/docs/uta/quick-start#access-setup).
## Configuration
The defaults target Bitget's regular UTA domains. A supplied `*http.Client` is used as-is; the SDK does not rewrite its timeout or transport.
| Option | Default | Use it for |
|---|---|---|
| `WithHTTPClient(*http.Client)` | Internal client with 15-second timeout | Proxy, custom TLS, tracing, or test transport |
| `WithTimeout(time.Duration)` | `15s` | Timeout of the internally created HTTP client |
| `WithBaseURL(string)` | `https://api.bitget.com` | A test server or Bitget VIP base origin |
| `WithDemoTrading()` | Off | REST requests made with a Demo API key |
| `WithLocale(string)` | `en-US` | Bitget's `locale` header |
| `WithLogger(Logger)` | No-op | Structured REST lifecycle logs |
| `WithWSURL(string)` | Public/private production URL | Demo, VIP, or test WebSocket endpoint |
| `WithWSAutoReconnect(bool)` | On | Disable automatic reconnect for a short-lived consumer |
| `WithWSLogger(Logger)` | No-op | Structured WebSocket lifecycle logs |
`WithBaseURL` expects an origin, not a path ending in `/api/v3`: endpoint methods add their own `/api/v3/...` path. Bitget documents the production, demo, and VIP domains in its [Quick Start](https://www.bitget.com/docs/uta/quick-start#domain-name).
```go
httpClient := &http.Client{Timeout: 30 * time.Second, Transport: myTransport}
client := bitget.NewRestClient("", "", "", bitget.WithHTTPClient(httpClient))
```
## Implemented REST coverage
This is a focused Phase 1 surface. Methods not listed here are not implemented yet.
| Service | Methods |
|---|---|
| `Market` (public) | `GetInstruments`, `GetTickers`, `GetOrderBook`, `GetCandles`, `GetHistoryCandles`, `GetPublicFills`, `GetFundingRateHistory`, `GetLiquidations` |
| `Account` (private) | `GetAssets`, `GetSettings`, `SetLeverage` |
| `Trade` (private) | `PlaceOrder`, `ModifyOrder`, `CancelOrder`, `GetOpenOrders`, `GetOrderHistory`, `GetPositions` |
See [docs/endpoints.md](docs/endpoints.md) for the exact HTTP paths and Bitget documentation for each method. Model fields are deliberately forward-compatible strings where a future API value should not be rejected by a closed enum.
### Deep candle history with response evidence (v1.2.0)
For candles older than the recent-data window, use `GetHistoryCandles`.
It calls the official `/api/v3/market/history-candles` route, with a request page
limit of 100. `GetCandles` still calls `/api/v3/market/candles` with its existing
maximum of 1,000. `GetFundingRateHistory` and custom `WithHTTPClient` transports
also keep their existing behaviour.
Use `GetHistoryCandlesWithReceipt` if you need both typed candles and evidence
of the exact response, without wrapping/intercepting an HTTP transport:
```go
// Imports: context, fmt, time, plus bitget, models and rest/market.
func readHistoricalCandles() error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client := bitget.NewRestClient("", "", "")
candles, receipt, err := client.Market.GetHistoryCandlesWithReceipt(ctx, market.GetHistoryCandlesOptions{
Category: models.CategorySpot,
Symbol: "BTCUSDT",
Interval: "1m",
CandleType: "market",
StartTime: "1609459200000",
EndTime: "1609465200000",
Limit: "100",
})
if receipt != nil {
fmt.Printf("HTTP %d, received %s, complete body: %t, bytes: %d\n",
receipt.StatusCode(), receipt.ReceivedAt(), receipt.Complete(), len(receipt.Payload()))
// Persist receipt.Payload() and receipt.Request() in your own evidence store.
}
if err != nil {
return err // Never treat an error response as admitted candle data.
}
for _, candle := range candles {
fmt.Printf("timestamp=%s close=%s volume=%s\n", candle[0], candle[4], candle[5])
}
return nil
}
```
The time selectors are Unix millisecond strings and are sent unchanged. Bitget
describes them as after/before bounds, but does not explicitly guarantee whether
equality is included. It also documents that `endTime` even 1 ms past an interval
boundary may add an earlier candle. The SDK preserves every returned row and its
order; it does not trim, round, deduplicate, or silently substitute recent data.
Each query window may span at most 90 days even though its data may be older.
See [Bitget's history contract](https://www.bitget.com/docs/catalog/market/market-data#get-klinecandlestick-history).
Pagination is explicit: choose the next time window from the returned timestamps,
handle overlapping boundary candles in your application, and stop when a page is
empty or makes no progress. The endpoint has no response cursor and the SDK does
not infer complete historical coverage from a full or empty page.
`models.RESTReceipt` stores at most `bitget.MaxRESTReceiptBytes` (10 MiB). Its
payload and request metadata accessors return copies. `ReceivedAt` is local time
after the bounded body read and before JSON decoding, separate from candle/event
time. Metadata contains method, path and allowlisted market selectors; it excludes
credentials, headers, host and URL userinfo. Bytes are the body exposed by
`net/http`, including its automatic decompression, not HTTP/TLS framing.
A receipt can accompany API errors, invalid JSON, interrupted reads and oversize
responses. Oversize/interrupted bodies have `Complete() == false`; oversize keeps
only the first 10 MiB and fails the call. A fully received error or malformed JSON
body can have `Complete() == true`: this flag describes byte capture, never market
coverage or successful decoding. Validation/cancellation before an HTTP response
returns a nil receipt. `market.NewClient` remains supported for custom injection,
but cannot provide receipts without `NewClientWithReceipts`; `NewRestClient`
wires both transports automatically.
### Liquidations are explicitly partial
`GetLiquidations` and `GetLiquidationsWithReceipt` call the official public
`/api/v3/market/liquidations` route and return `models.PartialLiquidations`.
Pass `market.GetLiquidationsOptions{Category: models.CategoryUSDTFutures,
Symbol: "BTCUSDT", Limit: "100"}` for a page, then pass the returned `Cursor`
unchanged in the next request. Cursors are opaque; stop on empty/non-progressing
pages rather than assuming that pagination proves completeness.
The [official REST contract](https://www.bitget.com/docs/catalog/market/derivatives#get-liquidations-history)
only covers the last three days and allows delayed data. It provides no stable
event ID in its documented response, and does not specify amount units or REST
aggregation/exhaustiveness. `Price`, `Amount` and `Ts` remain exact provider
strings. The SDK creates no execution identity, converts no units, and does not
deduplicate indistinguishable observations. Do not label these data complete
historical liquidations. WebSocket liquidation aggregation rules must not be
assumed to apply to REST.
`GetPublicFills` remains recent public trades, preserving `ExecID`, `ExecLinkID`,
`Ts` and category-dependent size units. The audited UTA market catalog has no
documented public deep fills route. The official `/api/v3/trade/fills` is private
account history, not the public market tape; no historical public fills method
is fabricated here. Audit sources and classified test fixtures are recorded in
[testdata/market-history/README.md](testdata/market-history/README.md).
### Pages, cursors, and cancellation
Order-list endpoints return a `Cursor`. Pass it back only after you have processed the current page. Give every request a deadline appropriate for your application.
```go
page, err := client.Trade.GetOpenOrders(ctx, trade.GetOpenOrdersOptions{
Category: models.CategorySpot,
Limit: "100",
})
if err != nil {
return err
}
for _, order := range page.List {
// Persist or handle order before asking for the next page.
}
if page.Cursor != "" {
// Request the next page with Cursor: page.Cursor.
}
```
Do not automatically retry an order placement after a timeout: the exchange may have accepted it even when your process did not receive a response. Use a unique `ClientOid` and then query the order state. For read-only requests, build your own retry policy around `ErrRateLimited`, context cancellation, and the endpoint-specific limits.
## Error handling
The package keeps Bitget's response code and message in `*bitget.Error`; use `errors.Is` for stable classifications and `errors.As` for details. A 429 response retains both `ErrRateLimited` and its API error payload when Bitget supplies one.
```go
assets, err := client.Account.GetAssets(ctx)
if err != nil {
if errors.Is(err, bitget.ErrRateLimited) {
// Back off according to the endpoint's documented limit.
return
}
var apiErr *bitget.Error
if errors.As(err, &apiErr) {
log.Printf("Bitget rejected the request: code=%s message=%s", apiErr.Code, apiErr.Message)
return
}
log.Printf("transport or decoding failure: %v", err)
}
```
Available classifications include `ErrUnauthorized`, `ErrInvalidSignature`, `ErrInvalidTimestamp`, `ErrPermissionDenied`, `ErrRateLimited`, `ErrInvalidParameter`, `ErrInsufficientFunds`, `ErrOrderNotFound`, and `ErrInternalServer`.
## WebSocket lifecycle
Use a context that you can cancel and always close the client. The client sends `ping`, expects `pong`, reconnects with exponential backoff after an unexpected disconnect, and resubscribes its active channels. It does not promise exactly-once or globally ordered delivery: make downstream processing idempotent, and deduplicate events using exchange IDs when a channel provides them.
```go
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
ws := bitget.NewPublicWSClient()
if err := ws.Connect(ctx); err != nil {
log.Fatal(err)
}
defer ws.Close()
pushes, err := ws.Subscribe(ctx, models.WSArg{
InstType: "SPOT",
Topic: "ticker",
Symbol: "BTCUSDT",
})
if err != nil {
log.Fatal(err)
}
for {
select {
case <-ctx.Done():
return
case push, ok := <-pushes:
if !ok {
return
}
fmt.Println(string(push.Data))
}
}
```
Each subscription has a 100-message buffer. If the consumer cannot keep up, the SDK drops the newest push and emits a warning through the configured WebSocket logger. For high-volume channels, process messages promptly, keep the handler non-blocking, and use a durable queue in your own application if loss is unacceptable.
For a lossless, provenance-sensitive path, configure `WithRawFrameHandler`.
It is called synchronously with an exact copied payload, immediate local receipt
time, and connection generation before the SDK decodes JSON or writes to a
subscriber buffer. Pair it with `WithWSEventHandler` to observe subscription
requests, Bitget ACKs/errors and terminal raw-observer failures by channel
`arg`. These callbacks deliberately apply backpressure; keep their work short
or hand off to your own durable queue. Mark a subscription ready only after a
`WSSubscribed` event for the current connection generation; an error with an
`arg` closes and removes that rejected subscription.
### Wait for a subscription ACK
`Subscribe` confirms that the SDK wrote the request; it does not mean Bitget
has accepted it. For a single, non-reconnecting consumer, wait for
`WSSubscribed` before treating the feed as ready. The example also surfaces a
provider rejection instead of silently continuing with an empty channel.
```go
// Imports: context, fmt, sync, plus bitget and models.
// The caller must close the returned client when it no longer needs the feed.
func subscribeWhenReady(ctx context.Context) (*bitget.WSClient, <-chan models.WSPush, error) {
ready := make(chan struct{})
providerError := make(chan error, 1)
var readyOnce sync.Once
ws := bitget.NewPublicWSClient(
bitget.WithWSAutoReconnect(false),
bitget.WithWSEventHandler(func(event bitget.WSLifecycleEvent) {
switch event.Type {
case bitget.WSSubscribed:
if event.Arg != nil && event.Arg.Topic == "ticker" && event.Arg.Symbol == "BTCUSDT" {
readyOnce.Do(func() { close(ready) })
}
case bitget.WSProviderError:
select {
case providerError <- fmt.Errorf("Bitget rejected subscription %#v: %s %s", event.Arg, event.Code, event.Message):
default: // Keep the synchronous lifecycle callback non-blocking.
}
}
}),
)
if err := ws.Connect(ctx); err != nil {
return nil, nil, err
}
pushes, err := ws.Subscribe(ctx, models.WSArg{InstType: "SPOT", Topic: "ticker", Symbol: "BTCUSDT"})
if err != nil {
_ = ws.Close()
return nil, nil, err
}
select {
case <-ready:
// The provider accepted this subscription.
return ws, pushes, nil
case err := <-providerError:
_ = ws.Close()
return nil, nil, err
case <-ctx.Done():
_ = ws.Close()
return nil, nil, ctx.Err()
}
}
```
Frequently Asked Questions
What is bitget-go?
bitget-go is a Financial library for the Go programming language. Go client for Bitget UTA API v3 with typed models, string-based prices, auto-reconnecting WebSocket, and demo trading
How do I install bitget-go?
Install bitget-go with the Go module system using `go get tigusigalpa/bitget-go`. Check the repository for the current installation instructions.
What category does bitget-go belong to?
bitget-go is listed under Financial, specifically Financial.