← Developer Blog

How to Build a Kalshi Tennis Trading Bot

Build a Kalshi tennis trading bot: use Kalshi's own API for event markets and execution, and a free live-scores API as the match-state signal layer.

· · By the Live Tennis API team

A Kalshi tennis trading bot has two halves: a read side that turns live match state into signals, and an execution side that talks to Kalshi's own API to place and manage orders on its tennis event markets. This guide wires the read side to a free live-scores feed and keeps execution — the regulated, account-bound part — as your responsibility. It's the Kalshi sibling of our Polymarket tennis bot pillar.

This is an engineering tutorial, not financial advice. There are no edges, profit claims, or guaranteed strategies here — only how to move data cleanly between a scores feed and a venue.

The two venues, side by side

Kalshi is a US-regulated exchange with its own public API and its own tennis event series (for example, per-match "who wins" markets around ATP/WTA events). Polymarket is a separate venue with a keyless Gamma market-data API and its own CLOB for execution. The pattern is the same on both: the venue owns the market and the order book; you supply the tennis intelligence.

Describe Kalshi's API only from its official docs — the exchange publishes endpoints for browsing markets, reading order books, and placing orders under an authenticated key tied to your account. Don't hardcode endpoint paths from a blog post (including this one); read them from Kalshi's current documentation, because auth and routes change.

What the free feed gives you

Live Tennis API is the match-state layer. On the free tier (30 req/min, 100/day) you get:

Base URL https://api.livetennisapi.com/api/public/v1, auth via an X-API-Key header. Grab a free key (no card) at livetennisapi.com/subscribe/free.

Two fields matter most for a bot, and neither is the lifecycle status alone. status only says upcoming / live / completed / cancelled. event_status (the feed's own designator: Retired, Walk Over, Cancelled, Postponed, Interrupted) and its closed-vocabulary companion outcome (completed, retired, walkover, default, abandoned; null until settled) are what tell your code a match ended abnormally. How fast they move depends on the case: an in-play retirement is stamped on the live ingest cycle, seconds after the feed states it; a walkover declared before first serve cannot be pushed by any live socket (the match never goes live), so it is picked up by our pre-start status poll — the current cadence is documented in the API docs' "Status freshness" section. On ULTRA the same change is pushed over the WebSocket (signals:["status"]) so you do not have to poll at all.

Step 1 — Read live match state

import requests

BASE = "https://api.livetennisapi.com/api/public/v1"
HEADERS = {"X-API-Key": "YOUR_FREE_KEY"}

def live_matches():
    r = requests.get(f"{BASE}/matches", params={"status": "live"}, headers=HEADERS, timeout=10)
    r.raise_for_status()
    return r.json()["data"]          # list endpoints return {"data": [...], "meta": {...}}

for m in live_matches():
    print(m["id"], m["status"], m.get("event_status"), m.get("outcome"))

Step 2 — Detect a retirement or walkover

A retirement (a player stops mid-match) or walkover (a player withdraws before play) is the sharpest event a tennis market has to price. How each venue resolves that outcome is set by the venue, not by us — Kalshi's tennis markets carry published resolution terms, exactly as Polymarket markets do (their descriptions spell out the resolution source and edge cases). Your job in code is not to assume the rule but to detect the event fast and then read the venue's rule before acting.

ABNORMAL = {"retired", "walkover"}   # values of `outcome`, NOT of `status`

def watch(match_ids):
    """Re-read each tracked match; yield (match_id, outcome) when it settles abnormally.

    Poll GET /matches/{id} for the ids you care about (a retired match leaves
    ?status=live the moment it ends, and a pre-start walkover was never in it),
    or use ?updated_since= as a change feed. On ULTRA, subscribe
    signals:["status"] on the WebSocket and skip polling entirely."""
    for mid in match_ids:
        r = requests.get(f"{BASE}/matches/{mid}", headers=HEADERS, timeout=10)
        r.raise_for_status()
        m = r.json()
        if m.get("outcome") in ABNORMAL:
            yield mid, m["outcome"], m.get("event_status"), m.get("withdrew")

def on_abnormal(match_id, outcome, event_status, withdrew):
    # A player retired or a walkover was declared. DO NOT assume how the
    # market resolves — look up THIS market's published rules on Kalshi,
    # then decide what your bot does about any open position.
    print(f"[{match_id}] {outcome} ({event_status}, withdrew={withdrew}): "
          "check the market's resolution terms")

seen = set()
for mid, outcome, event_status, withdrew in watch(TRACKED_IDS):
    if mid not in seen:
        seen.add(mid)
        on_abnormal(mid, outcome, event_status, withdrew)

Run watch() on an interval over the ids you hold positions in (build TRACKED_IDS from ?status=upcoming / ?status=live plus your market join), and route each first sighting through on_abnormal. That's your whole signal core — a small state machine over outcome/event_status, plus score deltas from /matches/{id}/score for momentum. Read Kalshi's official rules for what "retired" means for a given market; never let the bot infer resolution from a hardcoded assumption.

What Kalshi then pays is its rule, not ours: per the market's own rules_secondary (retrieved 2026-08-23), a pre-start walkover/cancellation resolves at a "fair price" on ATP/WTA match markets and at $0.50 on ITF match markets, and on ITF a player who withdraws or forfeits after a ball has been played resolves No. The verbatim text for Kalshi, polymarket.com and Polymarket US is in Polymarket & Kalshi tennis retirement/walkover rules (2026).

Step 3 — Match a fixture to a Kalshi market

Kalshi identifies markets by its own tickers and titles; the tennis feed identifies matches by player and fixture. Join them the same way our Polymarket toolkit does for Gamma: normalize player names, match on the pair plus event, and keep a manual override table for the awkward cases. The open-source toolkit is Polymarket-only and observe-only, but the matching approach ports directly to Kalshi's market listing.

Step 4 — Execution stays yours

Orders, keys, wallets, position state and reconciliation live entirely on your side of Kalshi's authenticated API. The scores feed never sees your account and places nothing. Build execution against Kalshi's current docs, handle partial fills and rejections, and gate any action on a fresh status read — a stale feed is the fastest way to trade into an event that already resolved.

Wrap-up

A Kalshi tennis bot is a clean split: Kalshi's API for markets and execution, a free live-scores feed for match state and abnormal-ending detection. Keep resolution logic pointed at each venue's published rules, keep the bot observe-first, and grow the read side before you ever automate a single order. See the Polymarket pillar for the cross-venue version and data-driven signals traders watch.

Frequently asked questions

Does Kalshi have tennis markets?

Yes. Kalshi is a US-regulated exchange that lists tennis event markets (such as per-match winner markets around ATP/WTA events) alongside its other event series. You browse and trade them through Kalshi's own authenticated API. A separate live-scores feed like Live Tennis API supplies the match state and score signals your bot reacts to; Kalshi supplies the market and the order book.

How does a Kalshi tennis bot get live scores and match status?

Point the read side at a live-data API. On Live Tennis API's free tier, GET /matches?status=live returns in-progress matches with a status field, and GET /matches/{id}/score returns the live score. Poll on an interval, diff snapshots to detect changes, and feed those signals to your logic. Base URL https://api.livetennisapi.com/api/public/v1 with an X-API-Key header; a free key needs no card.

What happens to a Kalshi tennis market on a retirement or walkover?

That is set by the market's published resolution rules on Kalshi, not by any third party, and it can differ by market — so read the specific market's terms rather than assuming. What your bot can do reliably is DETECT the event: the match's `outcome` field settles to 'retired' or 'walkover' (with `event_status` carrying the feed's own designator and `withdrew` naming the player), letting your code react and then apply the venue's own rule to any open position. An in-play retirement lands within seconds of the feed stating it; a walkover declared before first serve arrives via a short pre-start status poll rather than the live socket.

Can I reuse a Polymarket tennis bot for Kalshi?

The architecture ports directly: the read/signal layer (live scores, status detection, name-to-market matching) is venue-agnostic, so the same match-state code feeds both. The execution layer does not port — Kalshi and Polymarket have different APIs, auth, and market identifiers, so you write execution separately against each venue's official docs. Our open-source polymarket-tennis toolkit shows the matching approach, and it is observe-only.

Is Live Tennis API a trading platform?

No. It is a tennis data API only — live scores, match status, players, rankings, and fixtures. It never places orders, holds funds, or touches your Kalshi account. Execution, keys, wallets, and risk are entirely the builder's responsibility on the venue's own API.

Built with the Live Tennis API — real-time scores, players, odds and model win-probability for ATP, WTA, Challenger and ITF.

API reference SDKs on GitHub Plans from $9.99/mo