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 is needed to read them.
This article covers the read side. How to list tennis markets, and what a market object looks like. How to filter down to the ones you care about (live singles moneylines vs. tournament futures). Then 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 are "Player A vs Player B" for a specific match. Futures are "Will X win the US Open?" markets. Props are set winner and 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 want to 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 trading at the same time. That board held live Cincinnati Open singles, Challengers, ITF matches and US Open futures. Tennis runs close to year-round across the 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). Then 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.