Polymarket exposes every tennis market through its free, keyless Gamma API at https://gamma-api.polymarket.com. Tennis markets carry tag id 864. Query /markets (or /events) with tag_id=864&active=true&closed=false and you get the questions, outcomes, and current market prices as plain JSON — no wallet or account required to read them.
This article covers the read side: how to list tennis markets, what a market object actually looks like, how to filter down to the ones you care about (live singles moneylines vs. tournament futures), and how to parse the price fields — which are not what you'd expect. The Gamma API is public and rate-friendly for polite polling. You do not need our API or anyone's key to fetch market prices; you need it later, for the live match state you'll join against these prices.
One request with curl
Start by confirming the endpoint works. This returns two live tennis markets:
curl -s "https://gamma-api.polymarket.com/markets?tag_id=864&active=true&closed=false&limit=2" | python3 -m json.tool
The important query parameters:
tag_id=864— the tennis tag. This is the filter that scopes results to tennis.active=true&closed=false— currently tradeable markets only (drop resolved and archived ones).limit/offset— page through results; Gamma caps page size, so paginate for the full set.order=volumeNum&ascending=false— sort by volume to surface the busiest markets first.
The market shape (and the price gotcha)
A Gamma market object carries the fields you need to build a signal. Trimmed to the useful ones:
{
"id": "1088471",
"question": "Will Jannik Sinner win the 2026 Men's US Open?",
"slug": "will-jannik-sinner-win-the-2026-mens-us-open",
"conditionId": "0x65bbe9ee...",
"outcomes": "[\"Yes\", \"No\"]",
"outcomePrices": "[\"0.435\", \"0.565\"]",
"volumeNum": 257002.62,
"liquidityNum": 87623.83,
"endDate": "2026-09-13T00:00:00Z",
"active": true,
"closed": false,
"enableOrderBook": true,
"groupItemTitle": "Jannik Sinner"
}
The gotcha: outcomes and outcomePrices are JSON-encoded strings, not arrays. You have to json.loads() them a second time. A price of "0.435" means the market implies a ~43.5% probability of that outcome; the two prices in a binary market sum to roughly 1.0. Treat them as market-implied probabilities in the 0-1 range, not a decimal price format.
List and filter tennis markets in Python
import json
import requests
GAMMA = "https://gamma-api.polymarket.com"
TENNIS_TAG = 864
def fetch_tennis_markets(limit=500):
"""Page through all currently-open tennis markets on Polymarket."""
markets, offset, page = [], 0, 100
while True:
resp = requests.get(
f"{GAMMA}/markets",
params={
"tag_id": TENNIS_TAG,
"active": "true",
"closed": "false",
"limit": page,
"offset": offset,
},
timeout=15,
)
resp.raise_for_status()
batch = resp.json()
if not batch:
break
markets.extend(batch)
offset += page
if len(batch) < page or len(markets) >= limit:
break
return markets
def parse_prices(market):
"""outcomes / outcomePrices arrive as JSON strings — decode both."""
outcomes = json.loads(market.get("outcomes") or "[]")
prices = json.loads(market.get("outcomePrices") or "[]")
return dict(zip(outcomes, (float(p) for p in prices)))
markets = fetch_tennis_markets()
print(f"{len(markets)} open tennis markets")
for m in markets[:10]:
prices = parse_prices(m)
quote = " ".join(f"{name} {p:.3f}" for name, p in prices.items())
print(f"- {m['question']}\n {quote} (vol ${m.get('volumeNum', 0):,.0f})")
Separating live moneylines from futures
A tag_id=864 sweep mixes three kinds of market: head-to-head moneylines ("Player A vs Player B" for a specific match), futures ("Will X win the US Open?"), and props (set winner, total games). For an automated bot that reacts to live scores, you usually want the two-player moneylines. There's no single flag for that, so filter heuristically:
def is_binary_moneyline(market):
outcomes = json.loads(market.get("outcomes") or "[]")
# H2H match markets have exactly two player outcomes and a live order book;
# "Yes/No" futures also have two outcomes, so also require the order book
# and exclude the Yes/No pair.
if len(outcomes) != 2 or not market.get("enableOrderBook"):
return False
return {o.lower() for o in outcomes} != {"yes", "no"}
live_h2h = [m for m in markets if is_binary_moneyline(m)]
print(f"{len(live_h2h)} head-to-head moneyline markets")
Grouped tournament futures (like the US Open winner) come back as many Yes/No markets that share an event; the /events endpoint (same tag_id=864) groups those under one title if you'd rather present the whole field together.
The proof point: this is a real, active category
This isn't a thin niche. On 2026-08-18, a live tag_id=864 sweep of the Gamma API returned 400+ open tennis moneyline markets — live Cincinnati Open singles, Challengers, ITF matches, and US Open futures — trading simultaneously. Tennis runs close to year-round across ATP, WTA, Challenger and ITF tours, so there is almost always a live board to work against.
Skip the boilerplate: the discovery module
Our open-source toolkit, livetennisapi/polymarket-tennis (MIT), wraps everything above in a discovery module: it calls Gamma, decodes the string-encoded price fields, classifies each market (match / futures / doubles), and hands you normalized Python objects instead of raw dicts. It's observe-only — no order execution, no wallets, no keys.
pip install git+https://github.com/livetennisapi/polymarket-tennis
pmtennis discover --matches-only --moneyline-only
Or from Python:
from polymarket_tennis import GammaClient, discover_tennis_markets
with GammaClient() as gamma:
for market in discover_tennis_markets(gamma, market_types={"moneyline"},
matches_only=True):
print(market.question, market.prices)
Next: from prices to a signal
Market prices alone aren't a signal — you need the live match state (score, server, break point) to know whether a price is stale or ripe. Gamma gives you the price; the Live Tennis API gives you the live score. The free tier covers live matches and current scores — grab a free key (no card) and read the companion guide, Match a Polymarket tennis market to a live match, to join the two by player name. Placing orders — wallets, keys, the CLOB client — is your own responsibility and out of scope here.