kucoin-go
Go client for KuCoin UTA and Classic REST & WebSocket APIs with HMAC-SHA256 auth, string-typed prices, and typed error hierarchy
About kucoin-go
This large README is shown as a plain-text preview. Read the full README at the source
# KuCoin Golang SDK

[](https://github.com/tigusigalpa/kucoin-go/actions/workflows/ci.yml)
[](https://github.com/tigusigalpa/kucoin-go/actions/workflows/ci.yml)
[](https://github.com/tigusigalpa/kucoin-go/actions/workflows/ci.yml)
[](https://github.com/tigusigalpa/kucoin-go/actions/workflows/codeql.yml)
[](https://codecov.io/gh/tigusigalpa/kucoin-go)
[](https://pkg.go.dev/github.com/tigusigalpa/kucoin-go)
[](go.mod)
[](LICENSE)
**Live KuCoin market data in Go, as plain structs.** Point kucoin-go at a market
and just read: tickers, trades, candles, order books, mark prices and funding rates
arrive on ordinary Go channels, over a WebSocket connection that pings, reconnects
and resubscribes on its own.
```go
client := kucoin.NewClient()
session, err := client.Classic.Futures.Stream.DialPublic(ctx)
if err != nil {
log.Fatal(err)
}
defer session.Close()
ticks, err := session.SubscribeTickerV2(ctx, []string{"XBTUSDTM", "ETHUSDTM"})
if err != nil {
log.Fatal(err)
}
for tick := range ticks.C() {
fmt.Println(tick.Symbol, tick.BestBidPrice, tick.BestAskPrice)
}
```
There is no JSON to parse, no ping loop to write, no reconnect logic to get wrong and
no API key to create. When the connection drops, the library fetches a fresh token,
reconnects, subscribes again and keeps feeding the same channel — and it tells you
that it happened.
kucoin-go is an independent, hand-written Go client for KuCoin's **UTA (Unified
Trading Account)** and **Classic (Spot, Margin, Futures)** REST and WebSocket APIs.
It is written from KuCoin's current documentation and checked against the live
service; it is not a wrapper around the official Universal SDK or any framework.
---
## Contents
- [Why this exists](#why-this-exists)
- [What you get](#what-you-get)
- [What is covered](#what-is-covered)
- [Install](#install)
- [Quick start](#quick-start)
- [Futures market data over REST](#futures-market-data-over-rest)
- [Streaming cookbook](#streaming-cookbook)
- [Working with exact numbers](#working-with-exact-numbers)
- [Credentials and security](#credentials-and-security)
- [UTA versus Classic](#uta-versus-classic)
- [Accounts and orders over REST](#accounts-and-orders-over-rest)
- [Configuration](#configuration)
- [FAQ](#faq)
- [Where to find what](#where-to-find-what)
- [A few practical tips](#a-few-practical-tips)
- [Testing and development](#testing-and-development)
- [Compatibility and migration](#compatibility-and-migration)
- [Security, risk and legal notice](#security-risk-and-legal-notice)
---
## Why this exists
KuCoin's official Universal SDK is generated for many languages and both account
models at once. That breadth has a cost: names vary from one area to the next,
fields are loosely typed, and errors look different depending on which endpoint
you called.
kucoin-go goes the other way. Every endpoint and every channel is written by hand,
tested, and shaped like the rest of the SDK, so once you have used one part you know
how the others behave. Coverage grows more slowly, but what is here does what it
says.
If you need every KuCoin endpoint today, use the official SDK. If you want a careful
Go client — above all for market data — this one is for you. And if something you
need is missing, an issue (or a pull request) is the fastest way to get it added.
---
## What you get
**Market data, typed from end to end**
- Every WebSocket channel of KuCoin's current documentation for Classic Futures,
Classic Spot, Classic Margin and UTA has a `Subscribe…` method that returns a
`*stream.Subscription[T]`: a channel of structs, an `Err()` that tells you why it
ended, and a counter of anything it had to drop.
- The complete public Classic Futures market-data REST group (17 methods) and the
complete UTA v2 market-data REST group (23 methods).
- Local order books that stay in step with the exchange: KuCoin's documented
"snapshot plus updates" procedure, a sequence check on every update, and an
automatic rebuild after a gap or a reconnect.
**A connection that looks after itself**
- Heartbeats at the rhythm KuCoin asks for; any incoming frame counts as a sign of life.
- Reconnects with jittered backoff and a *fresh* connection token every time (tokens
expire after 24 hours, which is exactly when a long-running process needs one).
- Resubscribes everything after a reconnect, in order, and puts a marker into each
stream so you know updates may be missing at that point.
- Paces its own requests so it stays inside KuCoin's limit on client messages — even
while restoring hundreds of subscriptions at once.
- Gives every subscription its own bounded queue and an overflow policy you choose;
a slow consumer never stalls the connection or the other subscriptions, and every
dropped update is counted.
- `Close` ends every subscription and waits for the library's goroutines. Nothing leaks.
**Numbers you can trust**
- `types.Decimal` keeps prices and sizes exactly as KuCoin sent them — strings, bare
numbers and `1.0E-4` alike — compares them numerically and never routes them
through `float64`.
- Where KuCoin's documentation and the live service disagree, the library follows the
live service and says so in the Godoc (see
[documentation versus live behaviour](docs/STREAMING.md#kucoin-documentation-versus-live-behaviour)).
**Errors you can act on**
- Typed errors throughout: `errors.Is(err, stream.ErrTopicNotFound)`,
`errors.As(err, &serverErr)`, `errors.Is(err, transport.ErrRateLimited)`.
- Invalid requests (an unknown candle interval, a malformed symbol, more than 100
symbols in one subscription) are refused locally, before anything is sent.
**Safe by default**
- Only GET requests are retried automatically. Orders, cancellations, transfers and
withdrawals are sent exactly once.
- `clientOid` is never generated for you, so retry-sensitive workflows stay under your
control.
- Credentials, signatures and private request bodies are never logged.
- Nothing in this repository — tests, examples, CI — places a live order or moves funds.
<details>
<summary><b>Also in the box</b></summary>
- `context.Context` as the first parameter of every network call.
- An injectable `*http.Client`, `Clock` and `Logger`.
- An independent HMAC-SHA256 signer (`KC-API-SIGN` and the HMAC-signed
`KC-API-PASSPHRASE`), checked against known-answer vectors.
- A shared `transport.Executor`: response-envelope decoding, `ResponseMeta` (HTTP
status, KuCoin business code and message, request ID, `gw-ratelimit-*`,
`x-in-time`/`x-out-time`) and a typed error hierarchy.
- A conservative GET-only retry policy: exponential backoff with jitter and a bounded
total time.
- Local validation of obviously malformed order requests (a cancel without an ID, a
batch over KuCoin's 20-item limit): `orders.ErrOrderIDOrClientOidRequired`,
`orders.ErrTooManyBatchCancelItems`.
- Defensive decoding around confirmed documentation slips:
`positions.Position.Value()` copes with the `positionValue`/`positionMargin` naming
conflict, and `leverage.LeverageEntry.MarginMode` keeps KuCoin's literal `ISOLATE`
instead of quietly "correcting" it.
- Manifest-driven coverage maps ([docs/ENDPOINTS.md](docs/ENDPOINTS.md),
[docs/CHANNELS.md](docs/CHANNELS.md)): the build fails when a manifest row does not
match a real method and a real test.
</details>
---
## What is covered
The short version: **every WebSocket channel, and the complete Futures and UTA
market-data REST; Spot and Margin REST and the trading side only in part.**
| Market | REST market data | WebSocket, public | WebSocket, private |
|---|---|---|---|
| Classic Futures | **Complete** — 17 methods | **Complete** — 10 channels + a managed local order book | **Complete** — 6 channels |
| Classic Spot | Partial — 8 of 20 methods | **Complete** — 12 channels + a managed local order book¹ | **Complete** — 4 channels |
| Classic Margin | Partial — 5 of 7 methods | **Complete** — 2 channels | **Complete** — 6 channels |
| UTA v2 | **Complete** — 23 methods | **Complete** — 8 channels + a managed local order book | **Complete** — 7 channels |
¹ KuCoin serves the full Spot order-book snapshot to signed requests only, so the
managed Spot book needs API credentials.
Not provided: WebSocket order entry (placing, cancelling or amending orders over the
socket is trading, not market data — use the REST order endpoints), most UTA v2
account and trading REST, the broader Classic account and funding REST, and the
specialty domains (Earn, VIP Lending, Convert, Broker, Affiliate, Copy Trading).
The exact boundary — method by method, channel by channel, with links to KuCoin's
pages — is in the [coverage matrix](docs/API_COVERAGE.md), the
[REST method map](docs/ENDPOINTS.md) and the [WebSocket channel map](docs/CHANNELS.md).
We would rather ship a small, correct surface than a large, half-tested one; if your
use case needs something that is not there yet, please
[open an issue](https://github.com/tigusigalpa/kucoin-go/issues) — real demand decides
what gets built next.
---
## Install
```bash
go get github.com/tigusigalpa/kucoin-go
```
Go 1.22 or newer. The only dependencies are `gorilla/websocket` and `yaml.v3` (the
latter only for the documentation generator and the manifest tests).
---
## Quick start
Everything in this section works without an account or API keys.
### 1. One REST call
```go
package main
import (
"context"
"fmt"
"log"
kucoin "github.com/tigusigalpa/kucoin-go"
)
func main() {
ctx := context.Background()
client := kucoin.NewClient() // no credentials: public market data only
ticker, err := client.Classic.Futures.Market.GetTicker(ctx, "XBTUSDTM")
if err != nil {
log.Fatal(err)
}
fmt.Printf("XBTUSDTM last %s, best bid %s, best ask %s\n",
ticker.Price, ticker.BestBidPrice, ticker.BestAskPrice)
}
```
```text
XBTUSDTM last 84587.6, best bid 84592.5, best ask 84592.6
```
Prefer the UTA side? The same call through the current UTA v2 surface:
```go
tickers, err := client.UTA.V2.Market.GetTickers(context.Background(), utav2market.TradeTypeSpot, "BTC-USDT")
if err != nil {
log.Fatal(err)
}
fmt.Println(tickers.List[0].LastPrice)
```
### 2. A live stream
Trades of two contracts as they happen, until you press Ctrl+C:
```go
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
kucoin "github.com/tigusigalpa/kucoin-go"
)
func main() {
// Ctrl+C cancels ctx, and everything below winds down from there.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
client := kucoin.NewClient()
session, err := client.Classic.Futures.Stream.DialPublic(ctx)
if err != nil {
log.Fatal(err)
}
defer session.Close() // ends every subscription and waits for the library's goroutines
trades, err := session.SubscribeTrades(ctx, []string{"XBTUSDTM", "ETHUSDTM"})
if err != nil {
log.Fatal(err)
}
for {
select {
case t, ok := <-trades.C():
if !ok { // the subscription ended; Err says why (nil when you closed it yourself)
log.Printf("stream ended: %v", trades.Err())
return
}
fmt.Printf("%s %-4s %d @ %s\n", t.Symbol, t.Side, t.Size, t.Price)
case <-ctx.Done():
return
}
}
}
```
```text
XBTUSDTM buy 2 @ 84600
XBTUSDTM buy 4 @ 84600
ETHUSDTM buy 1 @ 2679.81
ETHUSDTM sell 77 @ 2680.02
```
A *session* is one WebSocket connection, and it carries any number of subscriptions
within KuCoin's per-connection limits. Sizes on Futures are in contracts — one XBTUSDTM
contract is 0.001 BTC.
### 3. A local order book
A full order book that the library keeps in step with the exchange, printed once a
second:
```go
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"time"
kucoin "github.com/tigusigalpa/kucoin-go"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
session, err := kucoin.NewClient().Classic.Futures.Stream.DialPublic(ctx)
if err != nil {
log.Fatal(err)
}
defer session.Close()
// The library subscribes to the level-2 feed, fetches the REST snapshot,
// replays what arrived in between and checks every sequence number from then on.
book, err := session.SubscribeOrderBook(ctx, "XBTUSDTM")
if err != nil {
log.Fatal(err)
}
<-book.Ready() // the first snapshot is in
every := time.NewTicker(time.Second)
defer every.Stop()
for {
select {
case <-every.C:
snap, ok := book.Book().SnapshotIfReady(3) // a consistent copy of the best 3 levels
if !ok || len(snap.Bids) == 0 || len(snap.Asks) == 0 {
fmt.Println("resynchronising…")
continue
}
spread, _ := snap.Asks[0].Price.Sub(snap.Bids[0].Price)
fmt.Printf("seq %d bid %s x %s ask %s x %s spread %s\n", snap.Sequence,
snap.Bids[0].Price, snap.Bids[0].Size, snap.Asks[0].Price, snap.Asks[0].Size, spread)
case <-book.Done():
log.Printf("order book ended: %v", book.Err())
return
case <-ctx.Done():
return
}
}
}
```
```text
seq 1748103244444 bid 84611.4 x 3469 ask 84611.5 x 3 spread 0.1
seq 1748103244567 bid 84611.4 x 3469 ask 84611.5 x 3 spread 0.1
seq 1748103245236 bid 84613.4 x 800 ask 84613.5 x 1 spread 0.1
```
If the stream ever skips a sequence number, or the connection drops, the book clears
itself, fetches a new snapshot and carries on. `SnapshotIfReady` reports `false`
during those few moments instead of handing you half a book.
**Runnable examples** with flags, signal handling and more output:
[futures_market](examples/futures_market/main.go) (a tour of the Futures REST data),
[futures_stream](examples/futures_stream/main.go),
[futures_orderbook](examples/futures_orderbook/main.go),
[uta_stream](examples/uta_stream/main.go),
[spot_stream](examples/spot_stream/main.go) and
[uta_v2_market](examples/uta_v2_market/main.go).
```bash
go run ./examples/futures_stream -symbols XBTUSDTM,ETHUSDTM -duration 30s
```
---
## Futures market data over REST
`client.Classic.Futures.Market` covers every public market-data endpoint of Classic
Futures. A few of them together:
```go
m := client.Classic.Futures.Market
// The contract: what one lot is worth, tick size, leverage, fees, live prices.
spec, err := m.GetSymbol(ctx, "XBTUSDTM")
if err != nil {
return err
}
fmt.Println("one contract is", spec.Multiplier, spec.BaseCurrency, "- tick", spec.TickSize, "- max leverage", spec.MaxLeverage)
// Six hours of 15-minute candles. Volume is in contracts, Turnover in USDT.
candles, err := m.GetKlines(ctx, market.KlineOptions{
Symbol: "XBTUSDTM",
Granularity: market.Granularity15Min,
From: time.Now().Add(-6 * time.Hour).UnixMilli(),
To: time.Now().UnixMilli(),
})
if err != nil {
return err
}
for _, c := range candles {
fmt.Println(c.Timestamp().Format("15:04"), c.Open, c.High, c.Low, c.Close, c.Volume)
}
// The best 20 levels (or 100). GetFullOrderBook returns the whole book.
book, err := m.GetPartOrderBook(ctx, "XBTUSDTM", 20)
if err != nil {
return err
}
fmt.Println("best bid", book.Bids[0].Price, "best ask", book.Asks[0].Price, "at sequence", book.Sequence)
// Mark price, the current funding rate and three days of settled rates.
mark, err := m.GetMarkPrice(ctx, "XBTUSDTM")
if err != nil {
return err
}
funding, err := m.GetCurrentFundingRate(ctx, "XBTUSDTM")
if err != nil {
return err
}
fmt.Println("mark", mark.Value, "funding", funding.Value, "next settlement", funding.NextFundingTime())
history, err := m.GetPublicFundingHistory(ctx, market.FundingHistoryOptions{
Symbol: "XBTUSDTM",
From: time.Now().Add(-72 * time.Hour).UnixMilli(),
To: time.Now().UnixMilli(),
})
if err != nil {
return err
}
for _, p := range history {
fmt.Println(p.Time().Format(time.DateTime), p.FundingRate)
}
```
(`market` is `github.com/tigusigalpa/kucoin-go/classic/futures/market`.)
Also there: all contracts (`GetAllSymbols`), all tickers (`GetAllTickers`), the last
100 trades (`GetTradeHistory`), the spot index, interest-rate and premium indices,
server time and service status. Candle granularities run from `Granularity1Min` to
`Granularity1Month`, including the 3-minute and one-month candles that the live API
serves although KuCoin's documentation leaves them out.
One quirk worth knowing: KuCoin documents `Get24hStats` (platform-wide 24-hour
turnover) as public, but only answers it for signed requests. The client signs it
when credentials are configured.
---
## Streaming cookbook
Short, complete recipes for the things people usually build first. Each one assumes
a `ctx`, a `client := kucoin.NewClient()` and, where it says `session`, a public
Futures session:
```go
session, err := client.Classic.Futures.Stream.DialPublic(ctx)
if err != nil {
log.Fatal(err)
}
defer session.Close()
```
The packages used below are `streaming` (`classic/futures/streaming`), `stream`,
`orderbook` and `utastream` (`uta/v2/streaming`), all under
`github.com/tigusigalpa/kucoin-go/`.
### Several channels at once
Give every subscription its own goroutine and start reading **right after**
subscribing. A subscription's queue begins to fill the moment KuCoin acknowledges
it; if you create five subscriptions first and only then start reading, the busy
ones may overflow in the meantime.
```go
// consume reads one subscription on a goroutine of its own, starting right away, so
// that its queue never waits for the next subscription to be created.
func consume[T any](wg *sync.WaitGroup, sub *stream.Subscription[T], handle func(T)) {
wg.Add(1)
go func() {
defer wg.Done()
for v := range sub.C() {
handle(v)
}
if err := sub.Err(); err != nil {
log.Printf("%s ended: %v", sub.Key(), err)
}
}()
}
func severalChannels(ctx context.Context, client *kucoin.Client) error {
symbols := []string{"XBTUSDTM", "ETHUSDTM", "SOLUSDTM"}
session, err := client.Classic.Futures.Stream.DialPublic(ctx)
if err != nil {
return err
}
var wg sync.WaitGroup
defer wg.Wait() // runs second: waits until every consumer has drained
defer session.Close() // runs first: ends the subscriptions, which closes their channels
ticks, err := session.SubscribeTickerV2(ctx, symbols)
if err != nil {
return err
}
consume(&wg, ticks, func(t streaming.TickerV2) {
fmt.Println(t.Symbol, "bid", t.BestBidPrice, "ask", t.BestAskPrice)
})
trades, err := session.SubscribeTrades(ctx, symbols)
if err != nil {
return err
}
consume(&wg, trades, func(t streaming.Trade) {
fmt.Println(t.Symbol, t.Side, t.Size, "@", t.Price)
})
<-ctx.Done()
return nil
}
```
One subscription can carry up to 100 symbols; updates for all of them arrive on the
same channel, each with its own `Symbol`.
### Only closed candles
KuCoin pushes the candle that is still open about once a second. When you only want
finished candles — to store them, say — keep the latest push per symbol and emit it
when the next period begins:
```go
candles, err := session.SubscribeKlines(ctx, streaming.Interval1Min, symbols)
if err != nil {
log.Fatal(err)
}
// KuCoin pushes the open candle about once a second. A candle is final when
// the next one starts, so remember the latest push per symbol and emit it then.
open := map[string]streaming.Kline{}
for k := range candles.C() {
if prev, ok := open[k.Symbol]; ok && prev.StartTime != k.StartTime {
fmt.Printf("%s %s O %s H %s L %s C %s %s contracts, %s USDT\n",Frequently Asked Questions
What is kucoin-go?
kucoin-go is a Financial library for the Go programming language. Go client for KuCoin UTA and Classic REST & WebSocket APIs with HMAC-SHA256 auth, string-typed prices, and typed error hierarchy
How do I install kucoin-go?
Install kucoin-go with the Go module system using `go get tigusigalpa/kucoin-go`. Check the repository for the current installation instructions.
What category does kucoin-go belong to?
kucoin-go is listed under Financial, specifically Financial.