Live Tennis API · docs · version 1.13.4

Tennis odds API — markets and price ticks

How do you read tennis match-winner odds and their price history? GET /markets returns the match-winner market for a match and GET /markets/{matchId}/prices the recent price ticks per side, newest first; GET /matches/{matchId}/prices is the same ticks addressed by match id. All three are PRO. Every tick carries its own timestamp, so a series can be reconstructed rather than inferred from poll times.

3 endpoints on this page. Base URL https://api.livetennisapi.com/api/public/v1; authenticate with the X-API-Key header. Plans involved: PRO. A free key needs no card.

GET /markets

Match-winner market(s) for a match (PRO)

Plan required: PRO · operationId: listMarkets

Parameters

GET /markets — parameters
NameInTypeRequiredNotes
match_idqueryintegeryes

Responses

GET /markets — responses
StatusMeaning
200Markets
400Bad query parameter
401Missing, unknown, or disabled credentials
403Your tier doesn't unlock this endpoint
429Rate limit exceeded (Retry-After header present). Three body shapes, told apart by error and scope: the per-MINUTE limit (rate_limited, with upgrade_url, tier and price naming the next tier up); the per-DAY quota (rate_limited with scope: "day", limit_per_day, and resets_at — the absolute ISO instant the daily window resets); and abuse_throttled with retry_at_epoch — a 24-hour block applied to clients that keep hammering far past their cap, which a well-behaved retry loop never sees. Fix the loop rather than retrying through it.

Response fields

GET /markets — response fields
FieldTypeDescription
dataarray of object
metaobject

Example

curl https://api.livetennisapi.com/api/public/v1/markets \
  -H "Authorization: Bearer twjp_..."

GET /markets/{matchId}/prices

Market + recent price ticks per side, newest first (PRO)

Plan required: PRO · operationId: getMarketPrices

The mapped match-winner market for one match, with its recent price ticks per side, newest first. 404 with error: no_market (since 2026-09-02; match_id and coverage: "none" alongside) when no match-winner market is mapped to the match — a market is mapped only where one is published for that fixture, so a client walking every live match will meet this. A bare not_found means the match id itself does not exist. Every row of GET /matches carries has_market, so a slate can be filtered before any call here. GET /matches/{matchId} answers 200 for the same match with market and market_price set to null instead, and carries the live score in the same response, so a per-match poll built on the detail route never spends a call on a 404. Ticks are kept for 30 days and then deleted: a market older than that answers with an empty prices array while staying mapped — retention, not a fault. For paging past 500 ticks use /matches/{matchId}/prices with its cursor.

Parameters

GET /markets/{matchId}/prices — parameters
NameInTypeRequiredNotes
matchIdpathintegeryesThe match id. Every match route shares ONE id space, so the same value works everywhere matchId appears. Get it from any match list, where it is the id field of each match object: GET /matches?status=live (in progress), GET /matches?status=upcoming or GET /fixtures (scheduled), GET /matches?status=completed or GET /history/matches (finished). Ids are stable for the life of a match, so one captured before it starts still resolves after it finishes.
limitqueryintegerno Default 50.

Responses

GET /markets/{matchId}/prices — responses
StatusMeaning
200Market with prices
401Missing, unknown, or disabled credentials
403Your tier doesn't unlock this endpoint
404error: not_found — no such match id. error: no_market (with match_id, coverage: "none", detail) — the match exists and no match-winner market is mapped to it; has_market on the match list says so without a probe.
410The match id existed and was merged into another match record (since 2026-09-05). The body is MatchMerged, not Error: it carries a forwarding address, which is the whole point of answering 410 rather than 404.
429Rate limit exceeded (Retry-After header present). Three body shapes, told apart by error and scope: the per-MINUTE limit (rate_limited, with upgrade_url, tier and price naming the next tier up); the per-DAY quota (rate_limited with scope: "day", limit_per_day, and resets_at — the absolute ISO instant the daily window resets); and abuse_throttled with retry_at_epoch — a 24-hour block applied to clients that keep hammering far past their cap, which a well-behaved retry loop never sees. Fix the loop rather than retrying through it.

Response fields

GET /markets/{matchId}/prices — response fields
FieldTypeDescription
idinteger
questionstring or null
statusstring or null (active, resolved, closed, null)
volumenumber or null
liquiditynumber or null
end_datestring or null
pricesarray of objectPrices endpoint / match detail only; newest first

Example

curl https://api.livetennisapi.com/api/public/v1/markets/18953/prices \
  -H "Authorization: Bearer twjp_..."

GET /matches/{matchId}/prices

Bare price ticks of the mapped match-winner market, newest first (PRO)

Plan required: PRO · operationId: listMatchPrices

Recent ticks only (no market wrapper). limit caps at 500; minutes bounds the lookback window. 404 when the match has no mapped market. Ticks are kept for 30 days and then deleted, so a match older than that answers an empty data while its market stays mapped — that is retention, not a fault. Past 500 ticks, page with the cursor: read a page, and while meta.has_more is true pass meta.next_cursor back as ?cursor= for the next (older) page. Pages never overlap or skip a tick (added 2026-09-09).

Parameters

GET /matches/{matchId}/prices — parameters
NameInTypeRequiredNotes
matchIdpathintegeryesThe match id. Every match route shares ONE id space, so the same value works everywhere matchId appears. Get it from any match list, where it is the id field of each match object: GET /matches?status=live (in progress), GET /matches?status=upcoming or GET /fixtures (scheduled), GET /matches?status=completed or GET /history/matches (finished). Ids are stable for the life of a match, so one captured before it starts still resolves after it finishes.
limitqueryintegerno Default 100.
minutesqueryintegerno
cursorquerystringnoKeyset cursor for the next (older) page — the meta.next_cursor of the previous page (added 2026-09-09). Opaque; anything else is 400 bad_cursor.

Responses

GET /matches/{matchId}/prices — responses
StatusMeaning
200Price ticks
400Bad query parameter
401Missing, unknown, or disabled credentials
403Your tier doesn't unlock this endpoint
404No such resource, or no data yet
410The match id existed and was merged into another match record (since 2026-09-05). The body is MatchMerged, not Error: it carries a forwarding address, which is the whole point of answering 410 rather than 404.
429Rate limit exceeded (Retry-After header present). Three body shapes, told apart by error and scope: the per-MINUTE limit (rate_limited, with upgrade_url, tier and price naming the next tier up); the per-DAY quota (rate_limited with scope: "day", limit_per_day, and resets_at — the absolute ISO instant the daily window resets); and abuse_throttled with retry_at_epoch — a 24-hour block applied to clients that keep hammering far past their cap, which a well-behaved retry loop never sees. Fix the loop rather than retrying through it.

Response fields

GET /matches/{matchId}/prices — response fields
FieldTypeDescription
dataarray of object
metaobject

Example

curl https://api.livetennisapi.com/api/public/v1/matches/18953/prices \
  -H "Authorization: Bearer twjp_..."