Kalshi lists tennis as a family of series: a market per player per match (KXATPMATCH, KXWTAMATCH, KXITFMATCH, KXATPCHALLENGERMATCH, KXWTACHALLENGERMATCH), set winners (KXATPSETWINNER), game totals, tiebreak and exact-score props, plus tournament futures. On 2026-08-23 the public API showed 34 open ATP, 26 WTA, 46 ITF, 70 ATP Challenger and 20 WTA Challenger match markets; the Cincinnati men's final (Fils vs Tiafoe) had $136k and $166k of contract volume on its two sides. Reads are keyless. Below: the ticker format, how to read prices and rules from the API, what the live-score side needs, and the settlement rules you must not hard-code.
Everything here is read-only plumbing, not advice. Execution on Kalshi needs your own authenticated API key and is your responsibility.
The tennis series on Kalshi (2026-08-23)
| Series ticker | What it is | Open markets seen |
|---|---|---|
KXATPMATCH |
ATP match winner, one market per player | 34 |
KXWTAMATCH |
WTA match winner | 26 |
KXITFMATCH / KXITFWMATCH |
ITF men's / women's match winner | 46 (men) |
KXATPCHALLENGERMATCH / KXWTACHALLENGERMATCH |
Challenger-level match winner | 70 / 20 |
KXATPSETWINNER / KXWTASETWINNER |
Set winner | 68 (ATP) |
KXATPGAMETOTAL, KXATPTIEBREAK, KXATPEXACTMATCH, KXATPTOTALSETS |
Game totals, tiebreak-to-occur, exact set score, total sets | varies by day |
KXATP / KXWTA, KXUSOPEN, KXWIMMEN, … |
Tournament winner futures | — |
Counts are from GET /trade-api/v2/markets?series_ticker=…&status=open on 2026-08-23; they change daily with the calendar. The full list of series (tennis included) is at GET /trade-api/v2/series?category=Sports.
Ticker anatomy
KXATPMATCH-26AUG23FILTIA-TIA
│ │ │ └─ outcome: the player this market pays on (TIA = Tiafoe)
│ │ └─ event: FILTIA = Fils vs Tiafoe
│ └─ date: 26AUG23 = 2026-08-23
└─ series
Each match is one event (KXATPMATCH-26AUG23FILTIA) with one market per player (…-FIL, …-TIA). The two markets are priced independently, so the bids do not have to sum to exactly $1.00 — on the Cincinnati final they were 0.66/0.67 (Fils) and 0.33/0.34 (Tiafoe).
Read prices, volume and rules — keyless
Kalshi's public market reads need no key. Prices come back in dollar strings (yes_bid_dollars, yes_ask_dollars, last_price_dollars), volume as volume_fp, and the settlement wording in rules_primary / rules_secondary.
import requests
K = "https://api.elections.kalshi.com/trade-api/v2"
def kalshi_tennis(series="KXATPMATCH", limit=50):
r = requests.get(f"{K}/markets", params={"series_ticker": series, "status": "open", "limit": limit}, timeout=10)
r.raise_for_status()
return r.json()["markets"]
for m in kalshi_tennis():
print(m["ticker"], m["yes_sub_title"],
"bid", m["yes_bid_dollars"], "ask", m["yes_ask_dollars"],
"vol", m["volume_fp"], "closes", m["close_time"])
# the rule text for THIS market — read it, never assume it
# print(m["rules_secondary"])
The order book is also public: GET /trade-api/v2/markets/{ticker}/orderbook returns orderbook_fp with yes_dollars / no_dollars price levels and sizes.
The rules: pre-start vs after a ball is played
From the ATP match market's own rules_secondary, retrieved 2026-08-23:
If the match does not occur (signaled by a ball being played) due to a player injury, walkover, forfeiture, or any other cancellation (all before the match starts), the market will resolve to a fair price in accordance with the rules. If this match is postponed or delayed, the market will remain open and close after the rescheduled match has finished (within two weeks).
The ITF series says something different:
… all markets will resolve to $0.50. If a player withdraws or forfeits after a match has started, that player will resolve to No.
So the two things a tennis trader on Kalshi must track are whether a ball has been played (the line between "fair price"/$0.50 and a normal result) and who retired. Side-by-side wording for Kalshi, polymarket.com and Polymarket US is in Polymarket & Kalshi tennis retirement/walkover rules (2026).
The live-score method
Kalshi's API tells you the price; it does not tell you the score. The method that works is to hold the Kalshi market next to the live match state and act on the state, not on the price alone:
- Map the ticker to a match. Split the event ticker into date + two surname fragments (
26AUG23+FIL/TIA) and match against today's fixtures by date and surname prefix; refuse to match when two candidates fit (shared surnames happen every week on the ITF tour). - Read live state on a schedule your tier allows. The Live Tennis API free tier (30 req/min, 100 req/day, no card) returns live matches with score line, server and break-point state; Ultra adds a push feed so you are not polling at all. Key at livetennisapi.com/subscribe/free.
- Watch the fields that change settlement:
outcome(retired,walkover,default,abandoned,completed;nulluntil settled),event_status(Retired,Walk Over,Cancelled,Postponed,Interrupted) andwithdrew. The lifecyclestatusonly ever saysupcoming/live/completed/cancelled. - Compare, then decide in your own code. What you do with a break point against the favourite or a retirement is your strategy; the data layer's job is to be right and early.
import os, requests
LTA = "https://api.livetennisapi.com/api/public/v1"
H = {"X-API-Key": os.environ["LIVETENNIS_API_KEY"]}
def live_board():
r = requests.get(f"{LTA}/matches", params={"status": "live"}, headers=H, timeout=10)
r.raise_for_status()
return r.json()["data"] # list endpoints return {"data": [...], "meta": {...}}
def surname(name: str) -> str:
return name.split()[-1].upper()
def find_match(event_ticker: str, board):
# KXATPMATCH-26AUG23FILTIA -> 'FILTIA' -> ('FIL', 'TIA')
frag = event_ticker.split("-")[1][7:]
a, b = frag[:3], frag[3:]
hits = [m for m in board
if {surname(m["players"]["p1"]["name"])[:3], surname(m["players"]["p2"]["name"])[:3]} == {a, b}]
return hits[0] if len(hits) == 1 else None # ambiguous or absent -> None, never a guess
for m in kalshi_tennis():
match = find_match(m["event_ticker"], live_board())
if match:
g = match.get("games") or [] # player-major: [[p1 games per set], [p2 games per set]]
line = " ".join(f"{a}-{b}" for a, b in zip(*g)) if g else match.get("status")
print(m["ticker"], m["yes_bid_dollars"], "|", line,
"| serving:", match.get("server"), "| outcome:", match.get("outcome"), match.get("event_status"))
The three-letter surname prefix is a heuristic that breaks on shared prefixes; the polymarket-tennis toolkit's matching module (full-name folding, date gating, explicit confidence, None on ambiguity) is the robust version and is venue-agnostic — point it at a Kalshi event ticker the same way.
How fast is Kalshi's tennis data — and how fast is the score?
These are two different latencies. Kalshi's API reflects its own order book immediately; the question traders actually ask is how far behind the court the score is. In the Live Tennis API an in-play change (point, break, retirement) is stamped on the live ingest cycle — seconds after the feed states it — while a walkover declared before first serve is picked up by a short pre-start status poll, because no live socket can push a match that never went live. We do not quote a single number here because it differs by tour and by case; the per-match event_status_updated_at timestamp lets you measure it yourself.
Related
- How to build a Kalshi tennis trading bot — the bot-shaped walkthrough
- Polymarket & Kalshi tennis retirement/walkover rules, verbatim (2026)
- Build a Polymarket tennis trading bot (Python) — same method, other venue
- Free live tennis scores for a trading bot