Python SDK
The Synpath Python library: one API for Kalshi, Polymarket, Polymarket US and Opinion, in process.
Installation
pip install synpathPython 3.10 or newer. One install covers market data, order entry, live streams, the execution engine and the REST server. Only the Polymarket US exchange API's gRPC streams are an extra: pip install "synpath[grpc]".
Quick Start
import synpath
client = synpath.Client() # market data needs no credentials
# Search one venue. Results are ranked by relevance and come back as a page.
page = client.fetch_markets(venue="kalshi", query="fed rate cut", limit=5)
for market in page:
print(f"ID: {market.id}, Title: {market.title}, YES ask: {market.yes.quote.ask}")
# Every id starts with its venue, so one call reaches any of them.
market = client.fetch_market("polymarket:2252244")
book = client.fetch_order_book(market.id, depth=5)
print(book.best_bid, book.best_ask)See Synpath IDs for the id format and Schemas for every field on a Market.
async: a strategy awaits its orders while its data calls stay plain. The per-venue clients (synpath.Kalshi(), KalshiTrading(...)) are what Client routes to and can be used directly.Credentials
Market data needs none. Order entry uses each venue's own key, read from the environment or a .env file next to your code, and never leaves your machine. Cross-venue matching and historical data are hosted by Synpath and take a Synpath API key, SYNPATH_API_KEY.
import synpath
client = synpath.Client(synpath.load_credentials()) # KALSHI_*, POLYMARKET_*, POLYMARKET_US_* from .envsynpath init # asks for your venue keys and writes .env, readable by you only
synpath doctor # which venues are configured; prints no secretThe variables each venue needs are on the Authentication page.
Trading
Place an Order
from synpath import OrderRequest
order = await client.create_order(OrderRequest(
market_id="polymarket:2252244",
side="buy", # buy takes YES, sell takes NO
type="limit", # limit or market
time_in_force="gtc", # gtc, ioc, fok, gtd
price="0.42", # always the YES price, 0 to 1
amount=10, # contracts
))
print(f"Order placed: {order.id} ({order.status})")Selling at 0.70 is the same order as buying NO at 0.30. On Polymarket, where YES and NO are separate tokens, sell buys the NO token; pass reduce_only=True to sell YES tokens you hold instead.
List Orders
for order in await client.fetch_open_orders(venue="polymarket"):
print(f"{order.id}: {order.status}, {order.filled}/{order.amount}")Get an Order
order = await client.fetch_order(order.id, market_id=order.market_id)
print(order.status, order.filled, order.remaining)Edit an Order
from synpath import EditRequest
order = await client.edit_order(EditRequest(order_id=order.id, price="0.41", amount=8), venue="polymarket")
print(order.queue_priority_preserved) # Kalshi keeps queue place on a size decrease; Polymarket never doesCancel an Order
cancelled = await client.cancel_order(order.id, market_id=order.market_id)
print(cancelled.status, cancelled.filled) # canceled, and whatever matched first
await client.cancel_all_orders(market_id="polymarket:2252244")Complex Orders
Stops, trailing stops, icebergs, one-cancels-the-other, brackets, TWAP, pegs, smart takers and orders on a bucket are held by the execution engine and sent to the venue as ordinary orders when their condition is met. The engine is your self-hosted server (synpath serve), which must be running; every parent is journaled there, so a restart resumes it where it was. Point the client at the server and the trading calls go through it.
synpath serve # your self-hosted server: holds your venue keys, runs the engine and the streamsfrom synpath import Client, OrderRequest, OrderType
client = Client(server="http://127.0.0.1:8000") # access token found on this machine; pass access_token= for a remote serverStop
stop = await client.create_order(OrderRequest(
market_id="kalshi:KXELONMARS-99", side="sell", amount=20,
type=OrderType.STOP_MARKET, stop_price="0.40",
params={"trigger_source": "touch", "max_slippage": "0.02"},
))
print(stop.status) # waiting, then triggeredTrailing stop
await client.create_order(OrderRequest(
market_id="kalshi:KXELONMARS-99", side="sell", amount=20,
type=OrderType.TRAILING_STOP, stop_price="0.40", params={"trail": "0.03"},
))Iceberg
await client.create_order(OrderRequest(
market_id="kalshi:KXELONMARS-99", side="buy", amount=1000, price="0.45",
type=OrderType.ICEBERG, params={"display": "50", "reload_delay_s": 2},
))One cancels the other
await client.create_order(OrderRequest(
market_id="kalshi:KXELONMARS-99", side="sell", amount=100, type=OrderType.OCO,
params={"legs": [
{"side": "sell", "type": "limit", "price": "0.60"}, # take profit
{"side": "sell", "type": "stop_market", "stop_price": "0.35"}, # stop loss
]},
))Bracket
await client.create_order(OrderRequest(
market_id="kalshi:KXELONMARS-99", side="buy", amount=100, type=OrderType.BRACKET,
params={"entry": {"type": "limit", "price": "0.40"},
"take_profit": {"price": "0.55"},
"stop_loss": {"stop_price": "0.30"}},
))TWAP
await client.create_order(OrderRequest(
market_id="kalshi:KXELONMARS-99", side="buy", amount=600, price="0.45",
type=OrderType.TWAP, params={"window_s": 1800, "slices": 12, "style": "limit"},
))Peg
await client.create_order(OrderRequest(
market_id="kalshi:KXELONMARS-99", side="buy", amount=100,
type=OrderType.PEG, params={"reference": "near", "max_price": "0.55", "min_stay_s": 5},
))Smart taker
await client.create_order(OrderRequest(
market_id="kalshi:KXELONMARS-99", side="buy", amount=500,
type=OrderType.SMART_TAKER, params={"clip": "50", "interval_s": 2, "limit": "0.48"},
))Every parameter each type takes is on the Create complex order page. Cancel the parent with await client.cancel_order(parent.id) and every live child is pulled.
Buckets
A bucket is one instrument made of the same proposition on several venues. A market order on it is routed across the members at the best prices net of fees and reported as one order with a weighted-average price. See the Smart Order Routing - Buckets concept page.
from synpath import BucketMember
bucket = await client.create_bucket(book="alpha", name="Fed cut in September", members=[
BucketMember(market_id="kalshi:KXFEDDECISION-26SEP-C25"),
BucketMember(market_id="polymarket:2252244", flip=True), # this listing is worded the other way round
])
order = await client.create_order(OrderRequest(
market_id=bucket.market_id, side="buy", amount=200, type=OrderType.MARKET, price="0.42", # the worst price you accept
))
report = await client.fetch_bucket_order(bucket.id, order.id) # filled, average, per venue
position = await client.fetch_bucket_position(bucket.id) # netted in bucket termsPortfolio
Positions
for p in await client.fetch_positions():
print(p.market_id, p.side, p.contracts, p.entry_price) # long holds YES, short holds NOFills
page = await client.fetch_my_trades(venue="polymarket")
for fill in page:
print(fill.market_id, fill.side, fill.price, fill.amount, fill.fee, fill.settlement)Balances
balance = await client.fetch_balance("kalshi")
print(balance.total, balance.available, balance.locked)P&L
Through the engine, whose ledger nets every fill on the YES leg and rolls it up:
engine.pnl("book") # per strategy
engine.pnl("market") # per contract
engine.pnl("account")Market Data
Search Markets
# One venue, one page
page = client.fetch_markets(venue="kalshi", query="bitcoin", limit=20, sort="volume")
next_page = client.fetch_markets(venue="kalshi", query="bitcoin", limit=20, cursor=page.next_cursor)
# Every venue's first page at once
page = client.fetch_markets(query="fed")
# Events, with their markets nested
events = client.exchange("polymarket").fetch_events(query="election", limit=10)| Parameter | Type | Description |
|---|---|---|
venue | str | None | kalshi, polymarket or polymarket_us. Omit for every venue's first page. |
query | str | None | Text filter, server-side on every venue, ranked by relevance. |
limit | int | None | Rows per page, up to 100. |
cursor | str | None | From a previous page's next_cursor. Needs venue. |
status | str | open (default), closed, settled or all. Polymarket cannot filter by settled. |
sort | str | None | volume, liquidity or newest. |
One Market
market = client.fetch_market("kalshi:KXELONMARS-99")
markets = client.fetch_markets_by_ids(["kalshi:KXELONMARS-99", "polymarket:2252244"]) # batched per venueOrder Book
book = client.fetch_order_book("polymarket:2252244", depth=10) # YES side
no = client.fetch_order_book("polymarket:2252244", side="no") # what NO costs
books = client.fetch_order_books(["polymarket:2252244", "polymarket:2252245"]) # one round tripTrades and OHLCV
trades = client.fetch_trades("kalshi:KXELONMARS-99", limit=100) # in the YES price
candles = client.fetch_ohlcv("kalshi:KXELONMARS-99", timeframe="1h", limit=24) # the newest 24 bars
history = client.fetch_ohlcv("kalshi:KXELONMARS-99", timeframe="1m",
since=1767225600000) # every bar since 2026-01-01
for c in candles:
print(c.datetime, c.open, c.close, c.volume, c.price_source) # read price_sourceFees
fee = client.fetch_fee_schedule("kalshi:KXELONMARS-99")
fee.estimate(price=0.50, contracts=100) # 1.75, before you tradeCapabilities
A capability a venue lacks raises NotSupported rather than returning an empty list. Ask first:
kalshi = client.exchange("kalshi")
kalshi.has["fetch_ohlcv"] # True, False or "partial"Historical Data
Tick-level historical order books and trades, from Synpath's hosted history service. Needs a Synpath API key, which you create with synpath login and synpath keys create (see Authentication). The client reads a saved key automatically; SYNPATH_API_KEY overrides it. Times are Unix milliseconds; coverage has gaps and varies by market, so a missing book comes back with an absence_reason, never as an empty book.
import synpath
at = synpath.fetch_order_book_at("kalshi:KXQUANTUM-30", as_of_ms=1789509599000)
print(at.book.best_bid if at.book else at.absence_reason)
trades = synpath.fetch_trades_range("kalshi:KXQUANTUM-30", start_ms, end_ms)
changes = synpath.fetch_order_book_range("kalshi:KXQUANTUM-30", start_ms, end_ms)Realtime (WebSockets)
Each venue has a stream class that turns its WebSocket into typed events and reconnects on its own. Subscribe with watch_* calls that take Synpath ids, then iterate.
import asyncio
from synpath import PolymarketMarketStream, BookEvent, TradeEvent
async def main():
async with PolymarketMarketStream() as stream:
await stream.watch_order_book(["polymarket:2252244"])
async for event in stream:
if isinstance(event, BookEvent):
print(event.side, event.best_bid, event.best_ask)
elif isinstance(event, TradeEvent):
print("trade", event.price, event.amount)
asyncio.run(main())Orders and fills stream the same way from KalshiStream, PolymarketUserStream and PolymarketUSPrivateStream. Every stream, event and guarantee is in the WebSocket API reference.
Errors
Every error is typed, and the type says whether a retry is reasonable. Through Client(server=...) the same exceptions are raised, rebuilt from your server's error body.
SynpathError
├── NetworkError no verdict from the venue; retrying is reasonable
│ ├── RequestTimeout
│ ├── RateLimitExceeded .retry_after when the venue says
│ └── ExchangeNotAvailable
├── ExchangeError the venue answered and said no
│ ├── BadRequest, MarketNotFound, AuthenticationError
│ ├── InvalidOrder off the tick, under the minimum
│ ├── OrderRejected the venue's own reason in .reason
│ └── InsufficientFunds, MarketHalted, OrderNotFound
├── RiskRejected the engine refused before sending; .rule names the rule
└── NotSupported this venue has no such capabilityimport synpath
try:
order = await client.create_order(request)
except synpath.RateLimitExceeded as exc:
await asyncio.sleep(exc.retry_after or 1) # back off and retry
except synpath.OrderRejected as exc:
print("refused:", exc.reason) # the venue's own word
