Documentation
¶
Overview ¶
Package kalshi is an unofficial Go client for Kalshi's trade-api v2 (api.elections.kalshi.com). It is not affiliated with Kalshi.
Without a Signer the client is strictly read-only public market data (NewClient — no credentials, nothing is ever sent to trading endpoints). With one (NewAuthedClient) the trading endpoints become available:
c := kalshi.NewClient(nil)
yesBid, yesAsk, err := c.FetchOrderbook(ctx, "KXBTCD-26JUL1612-T110000")
signer, err := kalshi.NewSigner(keyID, pemKey)
c = kalshi.NewAuthedClient(nil, signer, "")
res, err := c.CreateOrder(ctx, kalshi.Order{...})
Requests are signed per Kalshi's published API-key spec: RSA-PSS (SHA-256) over timestamp+method+path, sent in KALSHI-ACCESS-* headers.
Prices and counts ¶
Prices cross the wire as fixed-point dollar strings ("0.5600"). This package parses them to integer cents (PriceC, YesBid, ...) because binary contracts trade in 1–99¢ and integer math avoids float drift in P&L. Contract counts are subtler: orders placed through this client are whole contracts, but Kalshi counterparties trade fractional contracts, so every count the exchange reports about your orders — fills, remaining, positions — can come back fractional. Such fields carry both a whole-contract view (Remaining is the ceiling of the exact count, Filled its floor; together they reproduce a whole-contract ledger exactly) and the exchange's exact fixed-point value (RemainingFP, FilledFP, NetYesFP, ...).
Sides ¶
Orders use the V2 single-book vocabulary: SideBid buys YES, SideAsk sells YES. A NO bid at q¢ is the same order as a YES ask at 100−q¢; prices always quote the YES side.
Retries and idempotency ¶
Every order requires a ClientOrderID — it is what makes retries safe on the exchange side — and CreateOrder refuses to place one without it. All requests retry 429s with linear backoff. Canceling an order the exchange no longer knows returns ErrOrderNotFound (wrapped); the caller resolves whether it filled or was already canceled through Fills, never by assuming.
Demo environment ¶
Kalshi's demo exchange speaks the same protocol at a different host. Pass DemoBaseURL as NewAuthedClient's baseURL, with a key pair generated in a demo account (demo and production credentials are separate):
c := kalshi.NewAuthedClient(nil, signer, kalshi.DemoBaseURL)
Passing a nil signer with a base URL override yields an unauthenticated, read-only client against that host — the same mechanism the package's tests use to point at httptest servers.
Malformed data ¶
Fields that feed money math parse strictly: a filled order whose average price is unusable is an error, never a 0¢ cost basis. Account-wide reads (OpenOrders, Positions, Fills) instead degrade per item — an unparseable order, position, or fill reaches the caller flagged via its Malformed field rather than failing the page, because those reads back reconciliation and emergency-stop paths that must keep working even when one record is poison.
Index ¶
- Constants
- Variables
- func NormalizeTrades(trades []Trade)
- type CancelResult
- type Candle
- type Client
- func (c *Client) Balance(ctx context.Context) (int64, error)
- func (c *Client) CancelOrder(ctx context.Context, orderID string) (CancelResult, error)
- func (c *Client) CreateOrder(ctx context.Context, o Order) (OrderResult, error)
- func (c *Client) DiscoverActive(ctx context.Context, series string) ([]Market, error)
- func (c *Client) FetchCandlesticks(ctx context.Context, series, ticker string, startTS, endTS int64, ...) ([]Candle, error)
- func (c *Client) FetchMarket(ctx context.Context, ticker string) (Market, error)
- func (c *Client) FetchOrderbook(ctx context.Context, ticker string) (yesBid, yesAsk int, err error)
- func (c *Client) FetchOrderbookDepth(ctx context.Context, ticker string) (yes, no []Level, err error)
- func (c *Client) FetchSettled(ctx context.Context, series string, limit int) ([]Market, error)
- func (c *Client) FetchTrades(ctx context.Context, ticker string) ([]Trade, error)
- func (c *Client) FetchTradesSince(ctx context.Context, ticker string, since time.Time) ([]Trade, error)
- func (c *Client) Fills(ctx context.Context, since time.Time) ([]Fill, error)
- func (c *Client) OpenOrders(ctx context.Context) ([]OpenOrder, error)
- func (c *Client) Positions(ctx context.Context) ([]Position, error)
- type Fill
- type Level
- type Market
- type OpenOrder
- type Order
- type OrderResult
- type Position
- type Signer
- type Trade
Examples ¶
Constants ¶
const ( SideBid = "bid" // buy YES SideAsk = "ask" // sell YES (== buy NO at 100−price) )
Order sides in the V2 single-book vocabulary.
const ( TIFImmediateOrCancel = "immediate_or_cancel" TIFGoodTillCanceled = "good_till_canceled" )
Time-in-force values (verified against the V2 create-order spec 2026-07-22; the API also offers fill_or_kill, which nothing here needs).
const DefaultBaseURL = "https://api.elections.kalshi.com/trade-api/v2"
DefaultBaseURL is Kalshi's public trade-api v2 root.
const DemoBaseURL = "https://demo-api.kalshi.co/trade-api/v2"
DemoBaseURL is the trade-api v2 root of Kalshi's demo exchange, for use as NewAuthedClient's baseURL. Demo accounts issue their own API keys; production credentials do not work against demo.
Variables ¶
var ErrOrderNotFound = errors.New("kalshi: order not found")
ErrOrderNotFound marks a cancel whose order the exchange no longer knows as resting (already fully filled or already canceled). The caller must resolve the ambiguity through the fills endpoint, never by assuming.
var ErrPostOnlyCross = errors.New("kalshi: post-only order would cross")
ErrPostOnlyCross marks a post-only order the exchange refused because it would have crossed the book and taken — the join-only invariant enforced server-side. Benign and self-limiting: the quote simply does not rest.
Functions ¶
func NormalizeTrades ¶
func NormalizeTrades(trades []Trade)
NormalizeTrades fills the derived fields on trades decoded from a cached tape (which holds only the wire strings) and sorts them oldest-first.
Types ¶
type CancelResult ¶
type CancelResult struct {
OrderID string
ClientOrderID string
// ReducedBy is how many whole contracts the cancel removed — the
// remaining count at cancellation time, rounded the same way
// OpenOrder.Remaining is. Anything below the caller's believed remaining
// filled (or was reduced) before the cancel landed: that difference is a
// real position, discovered via Fills, never retried away.
ReducedBy int
// ReducedByFP is the exact reduced_by. It goes fractional when a
// counterparty took part of the lot before the cancel landed; the
// caller compares against it to notice pieces too small to move
// ReducedBy.
ReducedByFP float64
TSMs int64
}
CancelResult is the exchange's answer to an order cancel.
type Candle ¶
type Candle struct {
EndTS int64 // unix seconds, period end
YesBid int // close, cents (0 if no bid)
YesAsk int // close, cents (0 if no ask)
}
Candle is one period's quote, reduced to closing top-of-book in cents.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client reads public market data, and — when built by NewAuthedClient — places orders and reads the portfolio. The zero value is not usable.
func NewAuthedClient ¶
NewAuthedClient returns a Client that signs every request with signer, unlocking the trading endpoints. baseURL "" means production; tests and the demo environment pass their own.
func NewClient ¶
NewClient returns a read-only Client using hc (or a sane default if hc is nil).
Example ¶
Read public market data with no credentials.
package main
import (
"context"
"fmt"
"log"
kalshi "github.com/jaracah/kalshi-go"
)
func main() {
c := kalshi.NewClient(nil)
yesBid, yesAsk, err := c.FetchOrderbook(context.Background(), "KXBTCD-26JUL1612-T110000")
if err != nil {
log.Fatal(err)
}
fmt.Printf("yes %d¢ bid / %d¢ ask\n", yesBid, yesAsk)
}
Output:
func (*Client) Balance ¶
Balance returns the member's available balance in cents. Doubling as the cheapest authenticated call, it makes a good startup auth check.
func (*Client) CancelOrder ¶
CancelOrder cancels a resting order by id. A 404 returns ErrOrderNotFound (wrapped): the order is gone from the book — filled or already canceled — and the caller owes a fills poll to learn which.
func (*Client) CreateOrder ¶
CreateOrder places o and reports what executed synchronously (everything, for IOC; any immediate crossing fill, for GTC). self_trade_prevention stays "taker_at_cross" on every order: on the taker side it can only cancel our own taker order, and on the maker side it is the resting order a self-cross cancels against — never a fabricated self-fill.
Example ¶
Place a limit order with an authenticated client.
package main
import (
"context"
"fmt"
"log"
"os"
kalshi "github.com/jaracah/kalshi-go"
)
func main() {
signer, err := kalshi.NewSigner(
os.Getenv("KALSHI_KEY_ID"),
os.Getenv("KALSHI_PRIVATE_KEY"), // PEM, PKCS#1 or PKCS#8
)
if err != nil {
log.Fatal(err)
}
c := kalshi.NewAuthedClient(nil, signer, "") // "" = production
res, err := c.CreateOrder(context.Background(), kalshi.Order{
Ticker: "KXBTCD-26JUL1612-T110000",
Side: kalshi.SideBid, // buy YES
Count: 10,
PriceC: 55, // 55¢ limit; zero TimeInForce = immediate-or-cancel
// Required: it makes retrying a 429 or ambiguous transport failure
// safe — the exchange dedupes on it. Unique per logical order.
ClientOrderID: "d5f4f4a2-1f5e-4c3a-9b6d-8f2a7c1e0b42",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("order %s: filled %d @ %d¢, %d resting\n",
res.OrderID, res.FillCount, res.AvgPriceC, res.Remaining)
}
Output:
func (*Client) DiscoverActive ¶
DiscoverActive returns open markets in a series, soonest close_time first.
func (*Client) FetchCandlesticks ¶
func (c *Client) FetchCandlesticks(ctx context.Context, series, ticker string, startTS, endTS int64, periodMin int) ([]Candle, error)
FetchCandlesticks returns the historical quote series for a market over [startTS, endTS] (unix seconds) at periodMin-minute resolution. Quotes are the period close (the open carries occasional pre-trade artifacts).
func (*Client) FetchMarket ¶
FetchMarket returns the current summary for one market.
func (*Client) FetchOrderbook ¶
FetchOrderbook returns live top-of-book in cents: yesBid is the best price you could SELL yes at (highest yes bid); yesAsk is the best price you could BUY yes at, derived as 100 - bestNoBid (a No bid at q is a Yes offer at 100-q). Either is 0 when that side of the book is empty.
func (*Client) FetchOrderbookDepth ¶
func (c *Client) FetchOrderbookDepth(ctx context.Context, ticker string) (yes, no []Level, err error)
FetchOrderbookDepth returns the live book's full depth, best-priced level first on each side. The yes side holds resting bids to buy YES; the no side holds resting bids to buy NO (a NO bid at q is a YES offer at 100−q). The best NO level's size is the queue a joining maker rests behind. Like FetchOrderbook this endpoint is not CloudFront-cached.
func (*Client) FetchSettled ¶
FetchSettled returns up to limit recently-settled markets in a series, newest first (each carries a Result of "yes"/"no" and its open/close times). It pages with the API cursor so limit may exceed the 100-per-request cap — letting callers reach older history.
func (*Client) FetchTrades ¶
FetchTrades returns the complete public tape for ticker, oldest first, paging with the API cursor (1000 trades per page, brief pause between pages). The page cap is a runaway guard far above any real single-market tape; hitting it is an error — a silently truncated tape would corrupt downstream analysis.
func (*Client) FetchTradesSince ¶
func (c *Client) FetchTradesSince(ctx context.Context, ticker string, since time.Time) ([]Trade, error)
FetchTradesSince returns the ticker's prints strictly after since, oldest first — the incremental form a fill-polling loop wants (refetching whole tapes every pass would hammer the API). The page cap bounds one poll; a capped read returns what it has with an error so the caller can treat the tape as possibly-incomplete rather than silently truncated.
func (*Client) Fills ¶
Fills returns portfolio fills at or after since, oldest first, cursor- paged. min_ts is second-granular and treated inclusively; callers dedupe by FillID across polls (a restart's re-read makes duplicates normal).
Example ¶
Rest a post-only maker quote, then discover its fills asynchronously.
package main
import (
"context"
"errors"
"fmt"
"log"
"os"
"time"
kalshi "github.com/jaracah/kalshi-go"
)
func main() {
signer, err := kalshi.NewSigner(
os.Getenv("KALSHI_KEY_ID"),
os.Getenv("KALSHI_PRIVATE_KEY"),
)
if err != nil {
log.Fatal(err)
}
c := kalshi.NewAuthedClient(nil, signer, "")
ctx := context.Background()
res, err := c.CreateOrder(ctx, kalshi.Order{
Ticker: "KXBTCD-26JUL1612-T110000",
Side: kalshi.SideAsk, // sell YES == buy NO at 100−price
Count: 5,
PriceC: 95,
TimeInForce: kalshi.TIFGoodTillCanceled,
PostOnly: true, // reject instead of crossing
ClientOrderID: "0b7c9d1e-2f3a-4b5c-8d9e-6f1a2b3c4d5e",
})
if errors.Is(err, kalshi.ErrPostOnlyCross) {
return // would have taken; the quote simply does not rest
} else if err != nil {
log.Fatal(err)
}
fills, err := c.Fills(ctx, time.Now().Add(-time.Minute))
if err != nil {
log.Fatal(err)
}
for _, f := range fills {
if f.OrderID != res.OrderID {
continue
}
if f.Malformed != "" {
log.Printf("unbookable fill %s: %s", f.FillID, f.Malformed)
continue
}
// CountFP can be fractional even though the order was whole:
// counterparties trade fractional contracts.
fmt.Printf("filled %.2f @ %d¢ (fee %d¢)\n", f.CountFP, f.YesPriceC, f.FeeC)
}
}
Output:
func (*Client) OpenOrders ¶
OpenOrders returns every resting order, cursor-paged — the exchange's order-side truth for adoption and reconciliation.
type Fill ¶
type Fill struct {
FillID string
OrderID string
Ticker string
BookSide string // SideBid | SideAsk
IsTaker bool
// Count is the whole-contract count when the exchange reported an
// integral fill, else 0. CountFP is always the exact reported count:
// counterparties trade fractional contracts, so YOUR integral order can
// fill in fractional pieces (observed live 2026-07-24: a 1-lot ask
// filled as 0.95 + 0.05). Callers that book whole contracts accumulate
// CountFP per order and book as wholes complete.
Count int
CountFP float64
YesPriceC int
FeeC int
Time time.Time
// Malformed is non-empty when the exchange reported the fill in a shape
// that cannot be booked at all (missing count, unusable price). Such a
// fill still flows to the caller — erroring the whole poll would wedge
// the fills cursor on one poison fill forever (observed live
// 2026-07-24) — but its numbers are not trustworthy and the caller must
// treat the account as diverged.
Malformed string
}
Fill is one portfolio fill — the async discovery record carrying the exchange-reported price and fee.
type Market ¶
type Market struct {
Ticker string `json:"ticker"`
Title string `json:"title"`
// This endpoint reports quotes as dollar-denominated strings ("0.2400"),
// not integer-cent fields. NOTE: every summary field here — including these
// quotes and volume_fp — freezes at the window's open_time (the response is
// CloudFront-cached and the object isn't refreshed intra-window). For a live
// quote use FetchOrderbook; treat VolumeFP as open-time only.
YesBidDollars string `json:"yes_bid_dollars"`
YesAskDollars string `json:"yes_ask_dollars"`
VolumeFP string `json:"volume_fp"`
Status string `json:"status"`
Result string `json:"result"` // "yes" | "no" | ""
OpenTime string `json:"open_time"` // ISO-8601, window start
CloseTime string `json:"close_time"` // ISO-8601, window end
// Derived from the *_dollars strings by normalize(); callers work in cents.
YesBid int // cents, 0-100
YesAsk int // cents, 0-100
}
Market is one market, with the fields this client decodes.
func PickLive ¶
PickLive returns the soonest-closing market that is actually tradeable right now: its window has opened and it is not within buffer of its close. markets is assumed sorted by close_time ascending (as DiscoverActive returns them). Markets with unparseable times are skipped. ok is false when nothing is currently in-window.
type OpenOrder ¶
type OpenOrder struct {
OrderID string
ClientOrderID string
Ticker string
BookSide string // SideBid | SideAsk (the V2 book vocabulary)
YesPriceC int // YES-side limit price, cents
Remaining int // WHOLE contracts that can still fill from this order
Filled int // whole contracts filled so far
CreatedTime string // ISO-8601, exchange-reported
// RemainingFP and FilledFP are the exchange's exact counts, fractional
// whenever a counterparty has taken part of a lot. Remaining is their
// ceiling and Filled their floor, which together reproduce a
// whole-contract caller's view exactly: a 5-lot part-filled by 0.18
// rests at 4.82 == 5 wholes still to book, 0 booked so far.
RemainingFP float64
FilledFP float64
// Malformed is non-empty when a count could not be parsed at all. The
// order still reaches the caller — its id is enough to cancel it — but
// its counts are zero and must not be trusted.
Malformed string
}
OpenOrder is one resting order, carrying both whole-contract and exact fixed-point counts.
type Order ¶
type Order struct {
Ticker string
Side string // SideBid | SideAsk
Count int // whole contracts, > 0
PriceC int // YES-side limit price in cents, 1..99
// TimeInForce: "" (= TIFImmediateOrCancel) or TIFGoodTillCanceled.
TimeInForce string
// PostOnly, on a resting order, makes the exchange reject a placement
// that would cross instead of taking — a maker's join-only invariant
// enforced exchange-side (verified in the V2 spec 2026-07-22).
PostOnly bool
// ClientOrderID makes retries idempotent on the exchange side: a 429 or
// transport error can be retried with the same id without double-filling.
ClientOrderID string
}
Order is a limit order in this package's native units (integer cents and whole contracts). The zero TimeInForce is IOC: a taker order either fills at its snapshot price or misses — if the book moved, missing the fill is usually the correct outcome. Pass TIFGoodTillCanceled to rest on the book.
type OrderResult ¶
type OrderResult struct {
OrderID string
// FillCount is WHOLE contracts filled immediately (0 = book moved, no
// fill — or a purely fractional fill, which FillCountFP shows).
FillCount int
// Remaining is whole contracts still resting for a GTC order, or
// canceled back for an IOC one.
Remaining int
// FillCountFP and RemainingFP are the exchange's exact counts. They
// differ from the whole-contract fields when a fractional counterparty
// took part of the lot; callers that book wholes use the ints, callers
// that must agree with the exchange's own arithmetic use these.
FillCountFP float64
RemainingFP float64
AvgPriceC int // volume-weighted average fill price, cents (0 when no fill)
// TotalFeeC is the fee for the whole fill in cents, derived from the
// exchange's per-contract average (average_fee_paid × the exact
// fill_count, rounded to the nearest cent) — the closest the V2
// response gets to an exact total.
TotalFeeC int
TSMs int64 // matching-engine timestamp, epoch ms
}
OrderResult is the exchange's synchronous answer to an IOC order.
type Position ¶
type Position struct {
Ticker string
// NetYes is the whole-contract net position: positive = YES contracts
// held, negative = NO contracts held (the exchange's position_fp uses
// the same sign). Rounded toward zero when the position is fractional —
// NetYesFP carries the exact value.
NetYes int
// NetYesFP is position_fp verbatim. A fractional position is ROUTINE,
// not corruption: it is exactly what the account holds while a
// whole-contract order is part-filled by a fractional counterparty, and
// it persists until that order completes, is canceled, or settles.
// Callers reconciling against a whole-contract ledger must compare
// against this plus their unbooked piece remainders, never treat the
// fraction itself as divergence (observed live 2026-07-27).
NetYesFP float64
// ExposureC is the exchange's market_exposure in cents — the cost basis
// of the aggregate position, usable to approximate an average entry
// price when adopting positions at startup.
ExposureC int64
// Malformed is non-empty when position_fp could not be parsed at all —
// genuine API drift, not a fraction. Per-position, never a page error: a
// page error here would break startup position adoption and blind every
// reconcile pass for as long as the position exists (observed live
// 2026-07-24).
Malformed string
}
Position is one market's exchange-side position.
type Signer ¶
type Signer struct {
// contains filtered or unexported fields
}
Signer holds an API key id and its RSA private key, and signs requests in place. Safe for concurrent use.
type Trade ¶
type Trade struct {
TradeID string `json:"trade_id"`
CreatedTime string `json:"created_time"`
TakerSide string `json:"taker_side"` // "yes" | "no"
CountFP string `json:"count_fp"`
YesPriceDollars string `json:"yes_price_dollars"`
// Derived from the wire strings by normalize; callers use these. Excluded
// from JSON so cached tapes round-trip through the wire fields only.
Time time.Time `json:"-"`
Count float64 `json:"-"` // contracts; the API reports fractions ("935.19")
YesCents int `json:"-"`
}
Trade is one print from the public trade tape (GET /markets/trades). taker_side is only "which side crossed the spread" — the tape does not say whether the taker was informed.