-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreference.html
More file actions
739 lines (703 loc) · 80.5 KB
/
Copy pathreference.html
File metadata and controls
739 lines (703 loc) · 80.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Live Tennis API — Full API Reference (text)</title>
<meta name="description" content="Complete text reference for the Live Tennis API: every endpoint, parameter, response field and plan tier. Real-time tennis scores, players, rankings, match-winner odds and model win-probability for ATP, WTA, Challenger and ITF.">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://docs.livetennisapi.com/reference.html">
<meta property="og:type" content="article">
<meta property="og:title" content="Live Tennis API — Full API Reference">
<meta property="og:url" content="https://docs.livetennisapi.com/reference.html">
<meta property="og:image" content="https://docs.livetennisapi.com/banner.jpg">
<link rel="icon" href="favicon.ico" sizes="any">
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"TechArticle",
"headline":"Live Tennis API — Full API Reference",
"description":"Complete text reference for every Live Tennis API endpoint, parameter, response field and plan tier.",
"url":"https://docs.livetennisapi.com/reference.html",
"inLanguage":"en",
"isPartOf":{"@type":"WebSite","name":"Live Tennis API","url":"https://livetennisapi.com"},
"publisher":{"@type":"Organization","name":"Live Tennis API","url":"https://livetennisapi.com","logo":"https://docs.livetennisapi.com/icon-256.png"}}
</script>
<link rel="preload" href="fonts/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="fonts.css">
<style>
/* Design tokens (app/services/design_tokens.py). This page used to carry the
legacy marketing palette — every one of its six dark hexes was an exact key
in design_tokens.LEGACY, i.e. a colour the token module exists to retire. */
:root {
--bg:#0e0e0e; --panel:#141414; --struct:#2a2a2a;
--text:#e4e2e1; --muted:#84967e; --accent:#00ff41;
--r:6px; /* control radius; structure is square */
color-scheme: dark; /* was "normal", which invites the UA to
render form controls and scrollbars light */
}
* { box-sizing:border-box; }
body { margin:0; background:var(--bg); color:var(--text);
font:16px/1.65 'Inter',ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif; }
.wrap { max-width:960px; margin:0 auto; padding:40px 20px 80px; }
/* PROSE MEASURE. The wrap is 960px because the tables and code blocks need
it; running body text to that width gives ~110 characters a line, well past
the 45-75 the eye tracks comfortably. Only the prose is narrowed — tables,
pre and the endpoint sections keep the full width they need. */
main > p, main > ul:not(.toc), main > ol, .op > p, .banner > p, header > p { max-width:72ch; }
a { color:var(--accent); }
a:focus-visible, .scrollx:focus-visible, .skip-link:focus {
outline:2px solid var(--accent); outline-offset:2px; }
h1,h2,h3 { font-family:'Space Grotesk','Inter',ui-sans-serif,sans-serif; }
h1 { font-size:2rem; margin:0 0 .3em; }
h2 { margin-top:2.5em; padding-bottom:.3em; border-bottom:1px solid var(--struct); }
h3 { margin-top:2em; }
h4 { margin:1.4em 0 .4em; color:var(--muted); font-size:.85rem; text-transform:uppercase; letter-spacing:.08em; }
code { background:var(--panel); padding:.15em .4em; border-radius:var(--r); font-size:.9em;
font-family:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace; }
pre { background:var(--panel); border:1px solid var(--struct); padding:14px; border-radius:0;
overflow-x:auto; font-family:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace; }
pre code { background:none; padding:0; }
/* A long URL is better wrapped than hidden behind a scrollbar: it stays
copyable and every character is on screen. */
pre.urls { white-space:pre-wrap; overflow-wrap:anywhere; }
/* An inline <code> cannot scroll — it is inline. A long token inside one
(the /ws URL) pushed the whole document 44px wider than the viewport at
390px, which is a horizontal scrollbar on the PAGE, not in a block. */
code { overflow-wrap:anywhere; }
/* Wide blocks scroll in a focusable region rather than in the table itself,
so a keyboard can reach them. */
.scrollx { overflow-x:auto; max-width:100%; }
table { width:100%; border-collapse:collapse; margin:.6em 0 1.2em; }
th,td { text-align:left; padding:8px 10px; border-bottom:1px solid var(--struct);
vertical-align:top; font-size:.92rem; }
th { color:var(--muted); font-weight:600; }
/* Visually hidden, still announced. */
.vh { position:absolute; width:1px; height:1px; margin:-1px; padding:0;
overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0; }
.method { color:var(--accent); font-family:'JetBrains Mono',ui-monospace,monospace; }
.meta { color:var(--muted); font-size:.9rem; }
.summary { margin:.2em 0; }
.banner { background:var(--panel); border:1px solid var(--struct); border-left:3px solid var(--accent);
padding:14px 18px; border-radius:0; margin:1.5em 0; }
ul.toc { list-style:none; padding:0; }
ul.toc li { border-bottom:1px solid var(--struct); }
/* NAVIGATION TOUCH TARGETS. These stacked links were 19px tall and 11-12px
apart — the page's only chrome, and the hardest thing on it to hit. The two
inline links inside prose sentences are deliberately left alone: WCAG 2.5.8
exempts a link inline in a block of text, and padding them would break the
line box around them. */
ul.toc li a, .pagenav a, .footnav a {
display:flex; align-items:center; min-height:44px; padding:4px 2px; }
.pagenav, .footnav { display:flex; flex-wrap:wrap; gap:0 18px; margin:.4em 0; }
.skip-link { position:absolute; left:-9999px; top:0;
background:var(--panel); color:var(--accent); padding:12px 16px;
border:1px solid var(--struct); z-index:10; }
.skip-link:focus { left:8px; top:8px; }
/* THE TEACHING BLOCK. As a <pre> this was 62 columns wide and showed 35 of
them at 390px — every annotation sliced mid-word, which is precisely the
content a first-time reader needs most. As a two-column grid the alignment
that makes it teach survives on a wide screen, and on a narrow one the
explanation simply stacks under the code it explains. Nothing scrolls,
nothing is cut. */
.annot { display:grid; grid-template-columns:max-content 1fr; gap:2px 22px;
background:var(--panel); border:1px solid var(--struct);
padding:14px; margin:.6em 0 1.2em; overflow-x:auto; }
.annot dt { font-family:'JetBrains Mono',ui-monospace,monospace; font-size:.9em;
white-space:pre; margin:0; }
.annot dd { margin:0; color:var(--muted); font-size:.9em; align-self:center; }
@media (max-width:600px) {
.annot { grid-template-columns:1fr; gap:0; }
.annot dt { margin-top:.7em; white-space:pre-wrap; overflow-wrap:anywhere; }
.annot dt:first-child { margin-top:0; }
.annot dd { padding-bottom:.2em; }
}
/* Link-only table cells are targets, not prose, so they get the full 44px.
(The inline links inside sentences are left alone — see above.) */
.linkrow a { display:inline-flex; align-items:center; min-height:44px; }
/* SCROLL, OR WRAP. On a phone the code blocks showed as little as 54% of
their widest line, cut mid-token. Soft-wrapping them shows all of it, and
costs nothing on copy: a soft wrap is not a newline, so a copied curl
command is still byte-for-byte the command. Desktop has the room and keeps
the original hard lines. A table cannot wrap, so tables still scroll. */
@media (max-width:760px) {
pre { white-space:pre-wrap; overflow-wrap:anywhere; }
}
/* A horizontal scrollbar is invisible on touch until you already know it is
there, and the design system has no gradients to fade an edge with. So the
regions that genuinely still scroll get a scrollbar that is always drawn,
in tokens, plus a note in words. */
.scrollnote { display:none; color:var(--muted); font-size:.8rem; margin:-.4em 0 1em; }
@media (max-width:760px) {
.scrollnote { display:block; }
.scrollx { scrollbar-width:thin; scrollbar-color:var(--muted) var(--panel); }
.scrollx::-webkit-scrollbar { height:6px; -webkit-appearance:none; }
.scrollx::-webkit-scrollbar-track { background:var(--panel); }
.scrollx::-webkit-scrollbar-thumb { background:var(--muted); }
}
</style>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<div class="wrap">
<header>
<h1>Live Tennis API — Full Reference</h1>
<p class="meta">Version 1.0 · OpenAPI 3.1.0</p>
<nav class="pagenav" aria-label="Related pages">
<a href="./">Interactive reference</a>
<a href="./openapi.yaml">OpenAPI spec</a>
<a href="https://livetennisapi.com">livetennisapi.com</a>
</nav>
<div class="banner">
<p><strong>This is the plain-text reference.</strong> It contains the same content as the
interactive documentation but requires no JavaScript, so it can be read by search engines,
answer engines and any HTTP client.</p>
</div>
</header>
<!-- Contents. There used to be one list, of the twelve endpoints, buried under
the seventh of nine sections; the prose sections it sat below were not
listed anywhere and eight of the nine headings had no id to link to. -->
<nav aria-labelledby="contents-heading">
<h2 id="contents-heading">Contents</h2>
<ul class="toc">
<li><a href="#quickstart">Quickstart — no code required</a></li>
<li><a href="#base-url">Base URL</a></li>
<li><a href="#authentication">Authentication</a></li>
<li><a href="#plans">Plans</a></li>
<li><a href="#clients">Official client libraries</a></li>
<li><a href="#conventions">Conventions</a></li>
<li><a href="#endpoints">Endpoints</a> — all 15, with parameters and responses</li>
<li><a href="#websocket">WebSocket feed (ULTRA)</a></li>
<li><a href="#faq">FAQ — plans and data depth</a></li>
<li><a href="#schemas">Schemas</a></li>
</ul>
</nav>
<main id="main">
<p>Real-time tennis scores, player data, match-winner market prices, and
model-driven match analysis. Read-only. Coverage is every tour equally:
ATP, WTA, Challenger, ITF and the junior Grand Slam draws.</p><p>Access is tiered (FREE / BASIC / PRO / ULTRA). Each tier includes
everything in the tiers below it; the concrete deltas are:</p><p><code>FREE</code> — self-serve, no card (<a href="https://livetennisapi.com/subscribe/free">https://livetennisapi.com/subscribe/free</a>).
Live and upcoming matches, current scores, players, fixtures, and your own
usage stats. 30 requests/minute, 1,000/day. No historical results, no
market prices, no model fields, no WebSocket.</p><p><code>BASIC</code> — adds historical data: the completed-match listing
(<code>/history/matches</code>, and <code>status=completed</code> on <code>/matches</code>) and the
per-match point-by-point tape with the per-point model win-probability
(<code>/history/matches/{matchId}</code>). 60 requests/minute, 10,000/day.</p><p><code>PRO</code> — adds match events (<code>/matches/{matchId}/events</code>), market prices
(<code>/markets</code>, <code>/markets/{matchId}/prices</code>) and the pre-built monthly bulk
history packages (<code>/history/packages</code>). 300 requests/minute, 100,000/day.</p><p><code>ULTRA</code> — adds model analysis (<code>/matches/{matchId}/analysis</code>), the live
model fields (<code>win_probability_p1</code>, <code>danger</code>) on every score object, the
WebSocket push feed at <code>/ws</code>, and outbound webhooks (direct keys).
600 requests/minute, 500,000/day.</p><p>A call above your tier returns <code>403 {"error":"upgrade_required"}</code> — never
a silent empty result.</p><p>The <code>/history/*</code> endpoints are also sold standalone as the **Historical
Data API** (no live-API subscription required): **Starter** — single-match
point-by-point tape reads via the API (tape plus the model win-probability
per point), all tours (ATP/WTA/Challenger/ITF), one match per request, no
bulk downloads; **Pro** — everything in Starter plus bulk monthly package
downloads and higher rate limits; **Business** — everything in Pro plus
year-scale archive exports, top rate limits and priority support. One-off
1-month and 1-year access passes are available without a subscription.
Plans and prices: <a href="https://livetennisapi.com/historical-tennis-data-api">https://livetennisapi.com/historical-tennis-data-api</a></p><p>All timestamps are UTC ISO 8601 with a <code>Z</code> suffix. List endpoints return
<code>{data, meta}</code>; single resources return the object directly. Ignore
unknown fields — additive changes land within v1.</p><p>A native WebSocket live feed (ULTRA) exists at <code>/ws</code> under the same base
URL. Subscribe with <code>{"topics":["live-scores"]}</code> (or <code>"match:<id>"</code>); the
server acks with a <code>subscribed</code> frame, then pushes <code>score</code> frames on every
change plus a <code>ping</code> heartbeat roughly every 15s. Opt into extra signals
with <code>{"topics":["live-scores"],"signals":["break_point"]}</code> to also receive
<code>break_point</code> and <code>break_point_result</code> frames (schemas <code>BreakPoint</code> /
<code>BreakPointResult</code>). Without <code>signals</code>, score frames only.
</p>
<h2 id="quickstart">Quickstart — no code required</h2>
<p>Paste this into a browser, with your key on the end. That's the whole setup:
no install, no headers, works on a phone.</p>
<pre class="urls"><code>https://api.livetennisapi.com/api/public/v1/matches?status=live&token=YOUR_KEY</code></pre>
<p>You'll get every live match. Here is one, and how to read it:</p>
<dl class="annot">
<dt>"players": { "p1": { "name": "Chase Ferguson" },
"p2": { "name": "Scott Jones" } }</dt>
<dd>who is playing</dd>
<dt>"sets": [1, 0]</dt>
<dd>p1 leads one set to nil</dd>
<dt>"games": [[6, 3], [4, 4]]</dt>
<dd>first list is p1, second is p2 — so 6-4 in the first set, 3-4 in the second</dd>
<dt>"points": ["0", "0"]</dt>
<dd>the game in progress</dd>
<dt>"server": 1</dt>
<dd>p1 is serving (2 = p2)</dd>
</dl>
<p><strong>Every score array is player-major:</strong> the first list belongs to player 1,
the second to player 2. Once that clicks, the rest of the API reads the same way.</p>
<p>Two more you can click, swapping <code>21131</code> for any <code>id</code> from the list above:</p>
<pre class="urls"><code>https://api.livetennisapi.com/api/public/v1/matches/21131?token=YOUR_KEY
https://api.livetennisapi.com/api/public/v1/matches/21131/score?token=YOUR_KEY</code></pre>
<h2 id="base-url">Base URL</h2>
<pre class="urls"><code>https://api.livetennisapi.com/api/public/v1</code></pre>
<h2 id="authentication">Authentication</h2>
<p>Three ways to present your key — all equivalent. Use the header in code; use
<code>?token=</code> when you just want to click a link or test from a browser or phone.
The <code>/health</code> endpoint needs no key.</p>
<pre><code>Authorization: Bearer twjp_...
X-API-Key: twjp_...
?token=twjp_... in the URL — browser-friendly</code></pre>
<p class="meta">A key in a URL can end up in browser history, server logs and referrer
headers, so prefer a header for anything automated or shared. For trying the API out,
clicking a link is the fastest route and that trade-off is fine.</p>
<h2 id="plans">Plans</h2>
<p>Every plan includes everything in the plans below it. The table shows the
<strong>delta</strong> — exactly what each upgrade adds, and the request budget you get.</p>
<div class="scrollx" tabindex="0" role="region" aria-label="Plans and pricing">
<table><caption class="vh">Plans — what each tier adds over the one below, its rate limit and price</caption>
<thead><tr><th scope="col">Plan</th><th scope="col">Adds</th><th scope="col">Rate limit</th><th scope="col">Price</th></tr></thead><tbody>
<tr><th scope="row">FREE</th><td>The current state of the game: live & upcoming matches, current scores, players, fixtures, your usage stats. No history, no market prices, no model fields, no WebSocket.</td><td>30/min · 1,000/day</td><td>$0 — no card</td></tr>
<tr><th scope="row">BASIC</th><td>Historical data: the completed-match listing (<code>/history/matches</code>, <code>status=completed</code>) and the full per-match point-by-point tape with the model win-probability at every point (<code>/history/matches/{matchId}</code>).</td><td>60/min · 10,000/day</td><td>$9.99/mo</td></tr>
<tr><th scope="row">PRO</th><td>Match events, market prices (<code>/markets</code>), and the pre-built monthly bulk history packages (<code>/history/packages</code>, JSONL/CSV).</td><td>300/min · 100,000/day</td><td>$29.99/mo</td></tr>
<tr><th scope="row">ULTRA</th><td>Model analysis, live <code>win_probability_p1</code> + <code>danger</code> on every score, the WebSocket push feed, outbound webhooks.</td><td>600/min · 500,000/day</td><td>$99.99/mo</td></tr>
</tbody></table></div>
<p class="scrollnote">The table above scrolls sideways.</p>
<p>Calling an endpoint above your plan returns <code>403 {"error":"upgrade_required"}</code> —
never a silent empty result. <a href="https://livetennisapi.com/#pricing">See pricing</a>.</p>
<h3 id="history-plans">Historical Data API — standalone plans</h3>
<p>The <code>/history</code> endpoints are also sold on their own, without a live-API
subscription:</p>
<div class="scrollx" tabindex="0" role="region" aria-label="Historical Data API plans">
<table><caption class="vh">Historical Data API plans — what each adds</caption>
<thead><tr><th scope="col">Plan</th><th scope="col">Adds</th></tr></thead><tbody>
<tr><th scope="row">Starter</th><td>Single-match point-by-point tape reads via the API — the tape plus the model win-probability per point — for all tours (ATP, WTA, Challenger, ITF), one match per request. No bulk downloads.</td></tr>
<tr><th scope="row">Pro</th><td>Everything in Starter, plus bulk monthly package downloads and higher rate limits.</td></tr>
<tr><th scope="row">Business</th><td>Everything in Pro, plus year-scale archive exports, top rate limits and priority support.</td></tr>
<tr><th scope="row">One-off passes</th><td>1-month and 1-year access passes, no subscription.</td></tr>
</tbody></table></div>
<p class="scrollnote">The table above scrolls sideways.</p>
<p>Plans and prices: <a href="https://livetennisapi.com/historical-tennis-data-api">https://livetennisapi.com/historical-tennis-data-api</a>.</p>
<h3 id="alerts-plans">Break-point Alerts — hosted alerts, no code</h3>
<p>A hosted companion product that pushes break-point alerts to your channels
(the same signal the ULTRA WebSocket <code>break_point</code> frame carries, without
running a client). <strong>Free</strong>: high-swing break points only (probability swing
≥ 0.15), one delivery channel. <strong>Pro ($9.99/mo)</strong>: every break point — no
swing floor — to unlimited channels: Telegram, Discord, email, SMS, WhatsApp.
Details: <a href="https://livetennisapi.com">livetennisapi.com</a>.</p>
<h2 id="clients">Official client libraries</h2>
<div class="scrollx" tabindex="0" role="region" aria-label="Official client libraries">
<table class="linkrow"><caption class="vh">Official client libraries — install command and source repository</caption>
<thead><tr><th scope="col">Language</th><th scope="col">Install</th><th scope="col">Source</th></tr></thead><tbody>
<tr><th scope="row">Python</th><td><code>pip install livetennisapi</code></td><td><a href="https://github.com/livetennisapi/livetennisapi-python">livetennisapi-python</a></td></tr>
<tr><th scope="row">JavaScript / TypeScript</th><td><code>npm install livetennisapi</code></td><td><a href="https://github.com/livetennisapi/livetennisapi-js">livetennisapi-js</a></td></tr>
<tr><th scope="row">MCP server (LLM agents)</th><td><code>npx livetennisapi-mcp</code></td><td><a href="https://github.com/livetennisapi/livetennisapi-mcp">livetennisapi-mcp</a></td></tr>
</tbody></table></div>
<h2 id="conventions">Conventions</h2>
<ul>
<li>Timestamps are UTC ISO 8601 with a <code>Z</code> suffix.</li>
<li>List endpoints return <code>{data, meta}</code>; single resources return the object directly.</li>
<li><code>limit</code> defaults to 50; the API rejects anything above 200. Paginate with <code>offset</code>.</li>
<li><strong>Ignore unknown fields.</strong> Additive changes ship within <code>v1</code>, so a client that
rejects unrecognised fields will break. Every official SDK parses permissively.</li>
<li><strong>Score shape:</strong> <code>sets</code> is <code>[sets_p1, sets_p2]</code>.
<code>games</code> is <code>[games_p1, games_p2]</code> where each side is a <em>per-set</em> list —
so <code>[[6,3,2],[4,6,1]]</code> reads 6-4, 3-6, 2-1. It is player-major, not set-major.</li>
</ul>
<h2 id="endpoints">Endpoints</h2>
<ul class="toc">
<li><a href="#healthCheck"><code>GET /health</code> — Liveness probe (no auth)</a></li>
<li><a href="#listMatches"><code>GET /matches</code> — List matches by lifecycle status (FREE)</a></li>
<li><a href="#getMatch"><code>GET /matches/{matchId}</code> — Full match detail (FREE; +market PRO, +analysis ULTRA)</a></li>
<li><a href="#getMatchScore"><code>GET /matches/{matchId}/score</code> — Current score only — lowest-latency REST read (FREE)</a></li>
<li><a href="#listMatchEvents"><code>GET /matches/{matchId}/events</code> — Match events, newest first (PRO)</a></li>
<li><a href="#getMatchAnalysis"><code>GET /matches/{matchId}/analysis</code> — Model analysis for a match (ULTRA)</a></li>
<li><a href="#searchPlayers"><code>GET /players</code> — Search players by name (FREE)</a></li>
<li><a href="#getPlayer"><code>GET /players/{playerId}</code> — One player's bio + ranking + cached stats (FREE)</a></li>
<li><a href="#listMarkets"><code>GET /markets</code> — Match-winner market(s) for a match (PRO)</a></li>
<li><a href="#getMarketPrices"><code>GET /markets/{matchId}/prices</code> — Market + recent price ticks per side, newest first (PRO)</a></li>
<li><a href="#listCompletedMatches"><code>GET /history/matches</code> — Completed matches, newest first, with derived winner (BASIC)</a></li>
<li><a href="#getMatchTape"><code>GET /history/matches/{matchId}</code> — Full point-by-point tape for one match, with per-point model win-probability (BASIC)</a></li>
<li><a href="#listHistoryPackages"><code>GET /history/packages</code> — List the pre-built monthly bulk history packages (PRO)</a></li>
<li><a href="#getHistoryPackage"><code>GET /history/packages/{period}</code> — One monthly package — manifest, or the bulk file itself (PRO)</a></li>
<li><a href="#listFixtures"><code>GET /fixtures</code> — Upcoming scheduled fixtures, earliest first (FREE)</a></li>
</ul>
<section class="op" id="healthCheck">
<h3><span class="method">GET</span> <code>/health</code></h3>
<p class="summary">Liveness probe (no auth)</p>
<p class="meta">Plan required: <strong>—</strong> · operationId: <code>healthCheck</code></p>
<h4>Responses</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /health responses"><table><caption class="vh">GET /health — responses</caption><thead><tr><th scope="col">Status</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>200</code></td><td>OK</td></tr></tbody></table></div>
<h4>Response fields</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /health response fields"><table><caption class="vh">GET /health — response fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead><tbody><tr><td><code>status</code></td><td>string</td><td></td></tr><tr><td><code>version</code></td><td>string</td><td></td></tr></tbody></table></div>
<h4>Example</h4><pre><code>curl https://api.livetennisapi.com/api/public/v1/health</code></pre>
</section>
<section class="op" id="listMatches">
<h3><span class="method">GET</span> <code>/matches</code></h3>
<p class="summary">List matches by lifecycle status (FREE)</p>
<p class="meta">Plan required: <strong>FREE</strong> · operationId: <code>listMatches</code></p>
<p><code>status=live</code> and <code>status=upcoming</code> are the FREE current-state picture. <code>status=completed</code> pages historical results and is part of the paid History product — it requires BASIC (the same rule as <code>/history/matches</code>) and returns <code>403 upgrade_required</code> on a FREE key.</p>
<h4>Parameters</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /matches parameters"><table><caption class="vh">GET /matches — parameters</caption><thead><tr><th scope="col">Name</th><th scope="col">In</th><th scope="col">Type</th><th scope="col">Required</th><th scope="col">Notes</th></tr></thead><tbody><tr><td><code>status</code></td><td>query</td><td>string (live, upcoming, completed)</td><td>no</td><td><code>live</code> (default) and <code>upcoming</code> are FREE; <code>completed</code> requires BASIC — paging completed results is the history surface. Default <code>live</code>.</td></tr><tr><td><code>tour</code></td><td>query</td><td>string (atp, wta, challenger, itf, juniors)</td><td>no</td><td>Restrict results to one tour. Each value covers its singles and doubles draws, so <code>atp</code> includes ATP doubles and <code>juniors</code> covers the boys' and girls' Grand Slam draws. Omit for all tours. An unrecognised value is a 400 rather than a silent pass-through, so a caller never receives a tour it did not ask for. Applied before pagination, so <code>meta.count</code> reflects the filtered set.</td></tr><tr><td><code>limit</code></td><td>query</td><td>integer</td><td>no</td><td> Default <code>50</code>.</td></tr><tr><td><code>offset</code></td><td>query</td><td>integer</td><td>no</td><td> Default <code>0</code>.</td></tr></tbody></table></div>
<h4>Responses</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /matches responses"><table><caption class="vh">GET /matches — responses</caption><thead><tr><th scope="col">Status</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>200</code></td><td>Matches with latest score</td></tr><tr><td><code>400</code></td><td>Bad query parameter</td></tr><tr><td><code>401</code></td><td>Missing, unknown, or disabled credentials</td></tr><tr><td><code>403</code></td><td>Your tier doesn't unlock this endpoint</td></tr><tr><td><code>429</code></td><td>Rate limit exceeded (Retry-After header present)</td></tr></tbody></table></div>
<h4>Response fields</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /matches response fields"><table><caption class="vh">GET /matches — response fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead><tbody><tr><td><code>data</code></td><td>array of object</td><td></td></tr><tr><td><code>meta</code></td><td>object</td><td></td></tr></tbody></table></div>
<h4>Example</h4><pre><code>curl https://api.livetennisapi.com/api/public/v1/matches \
-H "Authorization: Bearer twjp_..."</code></pre>
</section>
<section class="op" id="getMatch">
<h3><span class="method">GET</span> <code>/matches/{matchId}</code></h3>
<p class="summary">Full match detail (FREE; +market PRO, +analysis ULTRA)</p>
<p class="meta">Plan required: <strong>FREE</strong> · operationId: <code>getMatch</code></p>
<h4>Parameters</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /matches/{matchId} parameters"><table><caption class="vh">GET /matches/{matchId} — parameters</caption><thead><tr><th scope="col">Name</th><th scope="col">In</th><th scope="col">Type</th><th scope="col">Required</th><th scope="col">Notes</th></tr></thead><tbody><tr><td><code>matchId</code></td><td>path</td><td>integer</td><td>yes</td><td></td></tr></tbody></table></div>
<h4>Responses</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /matches/{matchId} responses"><table><caption class="vh">GET /matches/{matchId} — responses</caption><thead><tr><th scope="col">Status</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>200</code></td><td>Match with score; <code>market</code> embed at PRO+, <code>analysis</code> embed at ULTRA</td></tr><tr><td><code>401</code></td><td>Missing, unknown, or disabled credentials</td></tr><tr><td><code>403</code></td><td>Your tier doesn't unlock this endpoint</td></tr><tr><td><code>404</code></td><td>No such resource, or no data yet</td></tr><tr><td><code>429</code></td><td>Rate limit exceeded (Retry-After header present)</td></tr></tbody></table></div>
<h4>Response fields</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /matches/{matchId} response fields"><table><caption class="vh">GET /matches/{matchId} — response fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead><tbody><tr><td><code>id</code></td><td>integer</td><td></td></tr><tr><td><code>tournament</code></td><td>string</td><td></td></tr><tr><td><code>surface</code></td><td>string or null (hard, clay, grass, null)</td><td></td></tr><tr><td><code>indoor</code></td><td>boolean</td><td></td></tr><tr><td><code>format</code></td><td>string or null (BO3, BO5, null)</td><td></td></tr><tr><td><code>round</code></td><td>string or null</td><td></td></tr><tr><td><code>status</code></td><td>string (upcoming, live, completed, cancelled)</td><td></td></tr><tr><td><code>event_status</code></td><td>string or null</td><td></td></tr><tr><td><code>is_doubles</code></td><td>boolean</td><td></td></tr><tr><td><code>scheduled_time</code></td><td>string or null</td><td></td></tr><tr><td><code>players</code></td><td>object</td><td></td></tr><tr><td><code>score</code></td><td>object or null</td><td></td></tr><tr><td><code>winner</code></td><td>integer or null</td><td>Completed matches only — derived from final sets</td></tr><tr><td><code>analysis</code></td><td>object</td><td>ULTRA only (absent below)</td></tr><tr><td><code>market</code></td><td>object or null</td><td>PRO+ only (absent below)</td></tr></tbody></table></div>
<h4>Example</h4><pre><code>curl https://api.livetennisapi.com/api/public/v1/matches/18953 \
-H "Authorization: Bearer twjp_..."</code></pre>
</section>
<section class="op" id="getMatchScore">
<h3><span class="method">GET</span> <code>/matches/{matchId}/score</code></h3>
<p class="summary">Current score only — lowest-latency REST read (FREE)</p>
<p class="meta">Plan required: <strong>FREE</strong> · operationId: <code>getMatchScore</code></p>
<h4>Parameters</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /matches/{matchId}/score parameters"><table><caption class="vh">GET /matches/{matchId}/score — parameters</caption><thead><tr><th scope="col">Name</th><th scope="col">In</th><th scope="col">Type</th><th scope="col">Required</th><th scope="col">Notes</th></tr></thead><tbody><tr><td><code>matchId</code></td><td>path</td><td>integer</td><td>yes</td><td></td></tr></tbody></table></div>
<h4>Responses</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /matches/{matchId}/score responses"><table><caption class="vh">GET /matches/{matchId}/score — responses</caption><thead><tr><th scope="col">Status</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>200</code></td><td>Current score (ULTRA adds win_probability_p1 + danger)</td></tr><tr><td><code>401</code></td><td>Missing, unknown, or disabled credentials</td></tr><tr><td><code>404</code></td><td>No such resource, or no data yet</td></tr><tr><td><code>429</code></td><td>Rate limit exceeded (Retry-After header present)</td></tr></tbody></table></div>
<h4>Response fields</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /matches/{matchId}/score response fields"><table><caption class="vh">GET /matches/{matchId}/score — response fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead><tbody><tr><td><code>sets</code></td><td>array of integer</td><td></td></tr><tr><td><code>games</code></td><td>array of array of integer</td><td>[games_p1, games_p2]; each a per-set list</td></tr><tr><td><code>points</code></td><td>array of string or null</td><td>In-game points as tennis strings ("0", "15", "40", "AD"). Entries can be NULL — observed live on completed matches, which also carry empty games arrays. Do not decode into non-nullable strings.</td></tr><tr><td><code>server</code></td><td>integer or null (1, 2, null)</td><td></td></tr><tr><td><code>is_tiebreak</code></td><td>boolean</td><td></td></tr><tr><td><code>win_probability_p1</code></td><td>number or null</td><td></td></tr><tr><td><code>danger</code></td><td>number or null</td><td></td></tr><tr><td><code>timestamp</code></td><td>string or null</td><td></td></tr></tbody></table></div>
<h4>Example</h4><pre><code>curl https://api.livetennisapi.com/api/public/v1/matches/18953/score \
-H "Authorization: Bearer twjp_..."</code></pre>
</section>
<section class="op" id="listMatchEvents">
<h3><span class="method">GET</span> <code>/matches/{matchId}/events</code></h3>
<p class="summary">Match events, newest first (PRO)</p>
<p class="meta">Plan required: <strong>PRO</strong> · operationId: <code>listMatchEvents</code></p>
<h4>Parameters</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /matches/{matchId}/events parameters"><table><caption class="vh">GET /matches/{matchId}/events — parameters</caption><thead><tr><th scope="col">Name</th><th scope="col">In</th><th scope="col">Type</th><th scope="col">Required</th><th scope="col">Notes</th></tr></thead><tbody><tr><td><code>matchId</code></td><td>path</td><td>integer</td><td>yes</td><td></td></tr><tr><td><code>limit</code></td><td>query</td><td>integer</td><td>no</td><td> Default <code>50</code>.</td></tr><tr><td><code>offset</code></td><td>query</td><td>integer</td><td>no</td><td> Default <code>0</code>.</td></tr></tbody></table></div>
<h4>Responses</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /matches/{matchId}/events responses"><table><caption class="vh">GET /matches/{matchId}/events — responses</caption><thead><tr><th scope="col">Status</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>200</code></td><td>Events</td></tr><tr><td><code>401</code></td><td>Missing, unknown, or disabled credentials</td></tr><tr><td><code>403</code></td><td>Your tier doesn't unlock this endpoint</td></tr><tr><td><code>429</code></td><td>Rate limit exceeded (Retry-After header present)</td></tr></tbody></table></div>
<h4>Response fields</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /matches/{matchId}/events response fields"><table><caption class="vh">GET /matches/{matchId}/events — response fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead><tbody><tr><td><code>data</code></td><td>array of object</td><td></td></tr><tr><td><code>meta</code></td><td>object</td><td></td></tr></tbody></table></div>
<h4>Example</h4><pre><code>curl https://api.livetennisapi.com/api/public/v1/matches/18953/events \
-H "Authorization: Bearer twjp_..."</code></pre>
</section>
<section class="op" id="getMatchAnalysis">
<h3><span class="method">GET</span> <code>/matches/{matchId}/analysis</code></h3>
<p class="summary">Model analysis for a match (ULTRA)</p>
<p class="meta">Plan required: <strong>ULTRA</strong> · operationId: <code>getMatchAnalysis</code></p>
<h4>Parameters</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /matches/{matchId}/analysis parameters"><table><caption class="vh">GET /matches/{matchId}/analysis — parameters</caption><thead><tr><th scope="col">Name</th><th scope="col">In</th><th scope="col">Type</th><th scope="col">Required</th><th scope="col">Notes</th></tr></thead><tbody><tr><td><code>matchId</code></td><td>path</td><td>integer</td><td>yes</td><td></td></tr></tbody></table></div>
<h4>Responses</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /matches/{matchId}/analysis responses"><table><caption class="vh">GET /matches/{matchId}/analysis — responses</caption><thead><tr><th scope="col">Status</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>200</code></td><td>Thesis + profile (either may be null)</td></tr><tr><td><code>401</code></td><td>Missing, unknown, or disabled credentials</td></tr><tr><td><code>403</code></td><td>Your tier doesn't unlock this endpoint</td></tr><tr><td><code>404</code></td><td>No such resource, or no data yet</td></tr><tr><td><code>429</code></td><td>Rate limit exceeded (Retry-After header present)</td></tr></tbody></table></div>
<h4>Response fields</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /matches/{matchId}/analysis response fields"><table><caption class="vh">GET /matches/{matchId}/analysis — response fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead><tbody><tr><td><code>thesis</code></td><td>object or null</td><td></td></tr><tr><td><code>profile</code></td><td>object or null</td><td></td></tr></tbody></table></div>
<h4>Example</h4><pre><code>curl https://api.livetennisapi.com/api/public/v1/matches/18953/analysis \
-H "Authorization: Bearer twjp_..."</code></pre>
</section>
<section class="op" id="searchPlayers">
<h3><span class="method">GET</span> <code>/players</code></h3>
<p class="summary">Search players by name (FREE)</p>
<p class="meta">Plan required: <strong>FREE</strong> · operationId: <code>searchPlayers</code></p>
<h4>Parameters</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /players parameters"><table><caption class="vh">GET /players — parameters</caption><thead><tr><th scope="col">Name</th><th scope="col">In</th><th scope="col">Type</th><th scope="col">Required</th><th scope="col">Notes</th></tr></thead><tbody><tr><td><code>search</code></td><td>query</td><td>string</td><td>no</td><td></td></tr><tr><td><code>limit</code></td><td>query</td><td>integer</td><td>no</td><td> Default <code>50</code>.</td></tr><tr><td><code>offset</code></td><td>query</td><td>integer</td><td>no</td><td> Default <code>0</code>.</td></tr></tbody></table></div>
<h4>Responses</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /players responses"><table><caption class="vh">GET /players — responses</caption><thead><tr><th scope="col">Status</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>200</code></td><td>Players (ranked first; no stats object on the list)</td></tr><tr><td><code>401</code></td><td>Missing, unknown, or disabled credentials</td></tr><tr><td><code>429</code></td><td>Rate limit exceeded (Retry-After header present)</td></tr></tbody></table></div>
<h4>Response fields</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /players response fields"><table><caption class="vh">GET /players — response fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead><tbody><tr><td><code>data</code></td><td>array of object</td><td></td></tr><tr><td><code>meta</code></td><td>object</td><td></td></tr></tbody></table></div>
<h4>Example</h4><pre><code>curl https://api.livetennisapi.com/api/public/v1/players \
-H "Authorization: Bearer twjp_..."</code></pre>
</section>
<section class="op" id="getPlayer">
<h3><span class="method">GET</span> <code>/players/{playerId}</code></h3>
<p class="summary">One player's bio + ranking + cached stats (FREE)</p>
<p class="meta">Plan required: <strong>FREE</strong> · operationId: <code>getPlayer</code></p>
<h4>Parameters</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /players/{playerId} parameters"><table><caption class="vh">GET /players/{playerId} — parameters</caption><thead><tr><th scope="col">Name</th><th scope="col">In</th><th scope="col">Type</th><th scope="col">Required</th><th scope="col">Notes</th></tr></thead><tbody><tr><td><code>playerId</code></td><td>path</td><td>integer</td><td>yes</td><td></td></tr></tbody></table></div>
<h4>Responses</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /players/{playerId} responses"><table><caption class="vh">GET /players/{playerId} — responses</caption><thead><tr><th scope="col">Status</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>200</code></td><td>Player with <code>stats</code> ({ratings, season})</td></tr><tr><td><code>401</code></td><td>Missing, unknown, or disabled credentials</td></tr><tr><td><code>404</code></td><td>No such resource, or no data yet</td></tr><tr><td><code>429</code></td><td>Rate limit exceeded (Retry-After header present)</td></tr></tbody></table></div>
<h4>Response fields</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /players/{playerId} response fields"><table><caption class="vh">GET /players/{playerId} — response fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead><tbody><tr><td><code>id</code></td><td>integer</td><td></td></tr><tr><td><code>name</code></td><td>string</td><td></td></tr><tr><td><code>tour</code></td><td>string or null</td><td>The record's OWN tour, which is NOT the <code>tour</code> filter vocabulary. It is granular (<code>juniors_boys</code>, <code>juniors_girls</code>, <code>challenger_men</code>) where the filter is grouped (<code>juniors</code>, <code>challenger</code>), and a doubles team reports it UPPERCASE (<code>ATP</code>) where an individual reports lowercase (<code>atp</code>). Treat it as an opaque string; do not parse it into the filter enum.</td></tr><tr><td><code>country</code></td><td>string or null</td><td></td></tr><tr><td><code>ranking</code></td><td>integer or null</td><td></td></tr><tr><td><code>ranking_points</code></td><td>integer or null</td><td></td></tr><tr><td><code>ranking_movement</code></td><td>string or null (up, down, same, null)</td><td></td></tr><tr><td><code>hand</code></td><td>string or null (R, L, null)</td><td></td></tr><tr><td><code>backhand</code></td><td>integer or null (1, 2, null)</td><td></td></tr><tr><td><code>birthday</code></td><td>string or null</td><td></td></tr><tr><td><code>is_doubles_team</code></td><td>boolean</td><td></td></tr><tr><td><code>data_completeness</code></td><td>object</td><td>How much biographical detail is known for this player, so a consumer can distinguish "not in the feed" from "not yet fetched" without probing. Present on every player in a match payload. Lower tours carry far less of it than main tour.</td></tr><tr><td><code>stats</code></td><td>object</td><td>Single-player endpoint only</td></tr></tbody></table></div>
<h4>Example</h4><pre><code>curl https://api.livetennisapi.com/api/public/v1/players/1104 \
-H "Authorization: Bearer twjp_..."</code></pre>
</section>
<section class="op" id="listMarkets">
<h3><span class="method">GET</span> <code>/markets</code></h3>
<p class="summary">Match-winner market(s) for a match (PRO)</p>
<p class="meta">Plan required: <strong>PRO</strong> · operationId: <code>listMarkets</code></p>
<h4>Parameters</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /markets parameters"><table><caption class="vh">GET /markets — parameters</caption><thead><tr><th scope="col">Name</th><th scope="col">In</th><th scope="col">Type</th><th scope="col">Required</th><th scope="col">Notes</th></tr></thead><tbody><tr><td><code>match_id</code></td><td>query</td><td>integer</td><td>yes</td><td></td></tr></tbody></table></div>
<h4>Responses</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /markets responses"><table><caption class="vh">GET /markets — responses</caption><thead><tr><th scope="col">Status</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>200</code></td><td>Markets</td></tr><tr><td><code>400</code></td><td>Bad query parameter</td></tr><tr><td><code>401</code></td><td>Missing, unknown, or disabled credentials</td></tr><tr><td><code>403</code></td><td>Your tier doesn't unlock this endpoint</td></tr><tr><td><code>429</code></td><td>Rate limit exceeded (Retry-After header present)</td></tr></tbody></table></div>
<h4>Response fields</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /markets response fields"><table><caption class="vh">GET /markets — response fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead><tbody><tr><td><code>data</code></td><td>array of object</td><td></td></tr><tr><td><code>meta</code></td><td>object</td><td></td></tr></tbody></table></div>
<h4>Example</h4><pre><code>curl https://api.livetennisapi.com/api/public/v1/markets \
-H "Authorization: Bearer twjp_..."</code></pre>
</section>
<section class="op" id="getMarketPrices">
<h3><span class="method">GET</span> <code>/markets/{matchId}/prices</code></h3>
<p class="summary">Market + recent price ticks per side, newest first (PRO)</p>
<p class="meta">Plan required: <strong>PRO</strong> · operationId: <code>getMarketPrices</code></p>
<h4>Parameters</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /markets/{matchId}/prices parameters"><table><caption class="vh">GET /markets/{matchId}/prices — parameters</caption><thead><tr><th scope="col">Name</th><th scope="col">In</th><th scope="col">Type</th><th scope="col">Required</th><th scope="col">Notes</th></tr></thead><tbody><tr><td><code>matchId</code></td><td>path</td><td>integer</td><td>yes</td><td></td></tr><tr><td><code>limit</code></td><td>query</td><td>integer</td><td>no</td><td> Default <code>50</code>.</td></tr></tbody></table></div>
<h4>Responses</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /markets/{matchId}/prices responses"><table><caption class="vh">GET /markets/{matchId}/prices — responses</caption><thead><tr><th scope="col">Status</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>200</code></td><td>Market with <code>prices</code></td></tr><tr><td><code>401</code></td><td>Missing, unknown, or disabled credentials</td></tr><tr><td><code>403</code></td><td>Your tier doesn't unlock this endpoint</td></tr><tr><td><code>404</code></td><td>No such resource, or no data yet</td></tr><tr><td><code>429</code></td><td>Rate limit exceeded (Retry-After header present)</td></tr></tbody></table></div>
<h4>Response fields</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /markets/{matchId}/prices response fields"><table><caption class="vh">GET /markets/{matchId}/prices — response fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead><tbody><tr><td><code>id</code></td><td>integer</td><td></td></tr><tr><td><code>question</code></td><td>string or null</td><td></td></tr><tr><td><code>status</code></td><td>string or null (active, resolved, closed, null)</td><td></td></tr><tr><td><code>volume</code></td><td>number or null</td><td></td></tr><tr><td><code>liquidity</code></td><td>number or null</td><td></td></tr><tr><td><code>end_date</code></td><td>string or null</td><td></td></tr><tr><td><code>prices</code></td><td>array of object</td><td>Prices endpoint / match detail only; newest first</td></tr></tbody></table></div>
<h4>Example</h4><pre><code>curl https://api.livetennisapi.com/api/public/v1/markets/18953/prices \
-H "Authorization: Bearer twjp_..."</code></pre>
</section>
<section class="op" id="listCompletedMatches">
<h3><span class="method">GET</span> <code>/history/matches</code></h3>
<p class="summary">Completed matches, newest first, with derived winner (BASIC)</p>
<p class="meta">Plan required: <strong>BASIC</strong> · operationId: <code>listCompletedMatches</code></p>
<p>Requires BASIC on the live API, or any Historical Data API plan (Starter and up — see <a href="https://livetennisapi.com/historical-tennis-data-api">https://livetennisapi.com/historical-tennis-data-api</a>). All tours. Filter to a date range with <code>from</code>/<code>to</code>.</p>
<h4>Parameters</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /history/matches parameters"><table><caption class="vh">GET /history/matches — parameters</caption><thead><tr><th scope="col">Name</th><th scope="col">In</th><th scope="col">Type</th><th scope="col">Required</th><th scope="col">Notes</th></tr></thead><tbody><tr><td><code>limit</code></td><td>query</td><td>integer</td><td>no</td><td> Default <code>50</code>.</td></tr><tr><td><code>offset</code></td><td>query</td><td>integer</td><td>no</td><td> Default <code>0</code>.</td></tr><tr><td><code>from</code></td><td>query</td><td>string</td><td>no</td><td>Earliest play date, <code>YYYY-MM-DD</code> or an ISO-8601 UTC datetime. A bare date covers that whole day. An unparseable value is a 400, never a silently unfiltered 200.</td></tr><tr><td><code>to</code></td><td>query</td><td>string</td><td>no</td><td>Latest play date, same formats as <code>from</code> (a bare date includes everything played that day). <code>from</code> after <code>to</code> is a 400.</td></tr></tbody></table></div>
<h4>Responses</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /history/matches responses"><table><caption class="vh">GET /history/matches — responses</caption><thead><tr><th scope="col">Status</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>200</code></td><td>Completed matches (<code>winner</code> = 1|2|null, from final sets)</td></tr><tr><td><code>400</code></td><td>Bad query parameter</td></tr><tr><td><code>401</code></td><td>Missing, unknown, or disabled credentials</td></tr><tr><td><code>403</code></td><td>Your tier doesn't unlock this endpoint</td></tr><tr><td><code>429</code></td><td>Rate limit exceeded (Retry-After header present)</td></tr></tbody></table></div>
<h4>Response fields</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /history/matches response fields"><table><caption class="vh">GET /history/matches — response fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead><tbody><tr><td><code>data</code></td><td>array of object</td><td></td></tr><tr><td><code>meta</code></td><td>object</td><td></td></tr></tbody></table></div>
<h4>Example</h4><pre><code>curl https://api.livetennisapi.com/api/public/v1/history/matches \
-H "Authorization: Bearer twjp_..."</code></pre>
</section>
<section class="op" id="getMatchTape">
<h3><span class="method">GET</span> <code>/history/matches/{matchId}</code></h3>
<p class="summary">Full point-by-point tape for one match, with per-point model win-probability (BASIC)</p>
<p class="meta">Plan required: <strong>BASIC</strong> · operationId: <code>getMatchTape</code></p>
<p>The complete chronological tape of a completed match — every recorded score row including the model fields <code>win_probability_p1</code> and <code>danger</code> at that point — plus match metadata and the model profiles produced during the match. The model fields here are part of the paid History product by design, distinct from the ULTRA-gated LIVE model fields. One match per request. Requires BASIC on the live API, or the Historical Data API Starter plan and up.</p>
<h4>Parameters</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /history/matches/{matchId} parameters"><table><caption class="vh">GET /history/matches/{matchId} — parameters</caption><thead><tr><th scope="col">Name</th><th scope="col">In</th><th scope="col">Type</th><th scope="col">Required</th><th scope="col">Notes</th></tr></thead><tbody><tr><td><code>matchId</code></td><td>path</td><td>integer</td><td>yes</td><td></td></tr></tbody></table></div>
<h4>Responses</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /history/matches/{matchId} responses"><table><caption class="vh">GET /history/matches/{matchId} — responses</caption><thead><tr><th scope="col">Status</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>200</code></td><td>The full tape (<code>match</code> + <code>tape</code> + <code>profiles</code> + <code>meta</code>)</td></tr><tr><td><code>401</code></td><td>Missing, unknown, or disabled credentials</td></tr><tr><td><code>403</code></td><td>Your tier doesn't unlock this endpoint</td></tr><tr><td><code>404</code></td><td>No such resource, or no data yet</td></tr><tr><td><code>429</code></td><td>Rate limit exceeded (Retry-After header present)</td></tr></tbody></table></div>
<h4>Response fields</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /history/matches/{matchId} response fields"><table><caption class="vh">GET /history/matches/{matchId} — response fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead><tbody><tr><td><code>match</code></td><td>object</td><td>Match metadata with the final score embedded.</td></tr><tr><td><code>tape</code></td><td>array of object</td><td>Chronological score rows, one per recorded point state.</td></tr><tr><td><code>profiles</code></td><td>array of object</td><td>Model profiles produced during the match, oldest first.</td></tr><tr><td><code>meta</code></td><td>object</td><td></td></tr></tbody></table></div>
<h4>Example</h4><pre><code>curl https://api.livetennisapi.com/api/public/v1/history/matches/18953 \
-H "Authorization: Bearer twjp_..."</code></pre>
</section>
<section class="op" id="listHistoryPackages">
<h3><span class="method">GET</span> <code>/history/packages</code></h3>
<p class="summary">List the pre-built monthly bulk history packages (PRO)</p>
<p class="meta">Plan required: <strong>PRO</strong> · operationId: <code>listHistoryPackages</code></p>
<p>Bulk downloads are a heavier product than single-match tape reads. Requires PRO on the live API, or the Historical Data API Pro plan and up, or a one-off package access pass. A key that can read the tape but is not package-entitled receives <code>403 upgrade_required</code>.</p>
<h4>Responses</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /history/packages responses"><table><caption class="vh">GET /history/packages — responses</caption><thead><tr><th scope="col">Status</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>200</code></td><td>Ready packages, newest month first</td></tr><tr><td><code>401</code></td><td>Missing, unknown, or disabled credentials</td></tr><tr><td><code>403</code></td><td>Your tier doesn't unlock this endpoint</td></tr><tr><td><code>429</code></td><td>Rate limit exceeded (Retry-After header present)</td></tr></tbody></table></div>
<h4>Response fields</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /history/packages response fields"><table><caption class="vh">GET /history/packages — response fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead><tbody><tr><td><code>data</code></td><td>array of object</td><td></td></tr><tr><td><code>meta</code></td><td>object</td><td></td></tr></tbody></table></div>
<h4>Example</h4><pre><code>curl https://api.livetennisapi.com/api/public/v1/history/packages \
-H "Authorization: Bearer twjp_..."</code></pre>
</section>
<section class="op" id="getHistoryPackage">
<h3><span class="method">GET</span> <code>/history/packages/{period}</code></h3>
<p class="summary">One monthly package — manifest, or the bulk file itself (PRO)</p>
<p class="meta">Plan required: <strong>PRO</strong> · operationId: <code>getHistoryPackage</code></p>
<p>Without <code>format</code> returns the package manifest (file set, counts, sha256). With <code>format=jsonl</code> or <code>format=csv</code> streams that file as an attachment. Same entitlement as <code>/history/packages</code>. 404 when the month has not been built yet — list available months first.</p>
<h4>Parameters</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /history/packages/{period} parameters"><table><caption class="vh">GET /history/packages/{period} — parameters</caption><thead><tr><th scope="col">Name</th><th scope="col">In</th><th scope="col">Type</th><th scope="col">Required</th><th scope="col">Notes</th></tr></thead><tbody><tr><td><code>period</code></td><td>path</td><td>string</td><td>yes</td><td>Calendar month, <code>YYYY-MM</code>.</td></tr><tr><td><code>format</code></td><td>query</td><td>string (jsonl, csv)</td><td>no</td><td>Omit for the JSON manifest; set to download the file.</td></tr></tbody></table></div>
<h4>Responses</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /history/packages/{period} responses"><table><caption class="vh">GET /history/packages/{period} — responses</caption><thead><tr><th scope="col">Status</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>200</code></td><td>The manifest (no <code>format</code>), or the bulk file as an attachment (<code>format=jsonl</code> streams NDJSON, <code>format=csv</code> streams CSV).</td></tr><tr><td><code>400</code></td><td>Bad query parameter</td></tr><tr><td><code>401</code></td><td>Missing, unknown, or disabled credentials</td></tr><tr><td><code>403</code></td><td>Your tier doesn't unlock this endpoint</td></tr><tr><td><code>404</code></td><td>No such resource, or no data yet</td></tr><tr><td><code>429</code></td><td>Rate limit exceeded (Retry-After header present)</td></tr></tbody></table></div>
<h4>Response fields</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /history/packages/{period} response fields"><table><caption class="vh">GET /history/packages/{period} — response fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead><tbody><tr><td><code>period</code></td><td>string</td><td>Calendar month, <code>YYYY-MM</code>.</td></tr><tr><td><code>status</code></td><td>string (ready)</td><td></td></tr><tr><td><code>match_count</code></td><td>integer</td><td></td></tr><tr><td><code>row_count</code></td><td>integer</td><td></td></tr><tr><td><code>files</code></td><td>array of object</td><td>One entry per downloadable format.</td></tr><tr><td><code>built_at</code></td><td>string or null</td><td></td></tr></tbody></table></div>
<h4>Example</h4><pre><code>curl https://api.livetennisapi.com/api/public/v1/history/packages/{period} \
-H "Authorization: Bearer twjp_..."</code></pre>
</section>
<section class="op" id="listFixtures">
<h3><span class="method">GET</span> <code>/fixtures</code></h3>
<p class="summary">Upcoming scheduled fixtures, earliest first (FREE)</p>
<p class="meta">Plan required: <strong>FREE</strong> · operationId: <code>listFixtures</code></p>
<h4>Parameters</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /fixtures parameters"><table><caption class="vh">GET /fixtures — parameters</caption><thead><tr><th scope="col">Name</th><th scope="col">In</th><th scope="col">Type</th><th scope="col">Required</th><th scope="col">Notes</th></tr></thead><tbody><tr><td><code>tour</code></td><td>query</td><td>string (atp, wta, challenger, itf, juniors)</td><td>no</td><td>Restrict results to one tour. Each value covers its singles and doubles draws, so <code>atp</code> includes ATP doubles and <code>juniors</code> covers the boys' and girls' Grand Slam draws. Omit for all tours. An unrecognised value is a 400 rather than a silent pass-through, so a caller never receives a tour it did not ask for. Applied before pagination, so <code>meta.count</code> reflects the filtered set.</td></tr><tr><td><code>limit</code></td><td>query</td><td>integer</td><td>no</td><td> Default <code>50</code>.</td></tr><tr><td><code>offset</code></td><td>query</td><td>integer</td><td>no</td><td> Default <code>0</code>.</td></tr></tbody></table></div>
<h4>Responses</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /fixtures responses"><table><caption class="vh">GET /fixtures — responses</caption><thead><tr><th scope="col">Status</th><th scope="col">Meaning</th></tr></thead><tbody><tr><td><code>200</code></td><td>Name-only fixtures (players not yet resolved to ids)</td></tr><tr><td><code>401</code></td><td>Missing, unknown, or disabled credentials</td></tr><tr><td><code>429</code></td><td>Rate limit exceeded (Retry-After header present)</td></tr></tbody></table></div>
<h4>Response fields</h4><div class="scrollx" tabindex="0" role="region" aria-label="GET /fixtures response fields"><table><caption class="vh">GET /fixtures — response fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead><tbody><tr><td><code>data</code></td><td>array of object</td><td></td></tr><tr><td><code>meta</code></td><td>object</td><td></td></tr></tbody></table></div>
<h4>Example</h4><pre><code>curl https://api.livetennisapi.com/api/public/v1/fixtures \
-H "Authorization: Bearer twjp_..."</code></pre>
</section>
<h2 id="websocket">WebSocket feed (ULTRA)</h2>
<p>A native WebSocket live feed is available at <code>https://api.livetennisapi.com/api/public/v1/ws</code>. Subscribe with
<code>{"topics":["live-scores"]}</code> or
<code>{"topics":["match:<id>"]}</code>. The server acknowledges with a
<code>subscribed</code> frame, then pushes <code>score</code> frames on every change plus a
<code>ping</code> heartbeat roughly every 15 seconds.</p>
<p>Opt into extra signals by adding a <code>signals</code> array to the subscribe frame —
<code>{"topics":["live-scores"],"signals":["break_point"]}</code> — to also receive a
<code>break_point</code> frame the instant a break point arises and a
<code>break_point_result</code> frame when it resolves. Their shapes are the
<code>BreakPoint</code> and <code>BreakPointResult</code> schemas below. Without
<code>signals</code> the feed pushes <code>score</code> frames only, exactly as before.</p>
<h2 id="faq">FAQ — plans and data depth</h2>
<h3 id="faq-how-much">How much data can I access on each plan?</h3>
<p><strong>FREE</strong> sees the current state of the game only — live and upcoming matches,
scores, players and fixtures — at 1,000 requests/day. <strong>BASIC</strong> adds every
completed match and its full point-by-point tape (with the model
win-probability at every point), one match per request, at 10,000/day.
<strong>PRO</strong> adds whole months of history in a single bulk file (JSONL or CSV),
plus match events and market prices, at 100,000/day. <strong>ULTRA</strong> adds model
analysis, the live model fields and the WebSocket push feed, at 500,000/day.
Coverage is identical on every plan: all tours, ATP through ITF — the plans
differ in which data products and volumes they unlock, never in which
tournaments you see.</p>
<h3 id="faq-how-far-back">How far back does history go?</h3>
<p><code>/history/matches</code> pages every completed match on record, all tours, newest
first — filter a window with <code>from</code>/<code>to</code>. Bulk packages are built per calendar
month; <code>GET /history/packages</code> lists exactly which months are available right
now, with match and row counts per month, and is always the authoritative
answer. Year-scale archive exports are part of the Historical Data API
Business plan.</p>
<h3 id="faq-whats-in-tape">What's in the point-by-point tape?</h3>
<p>One row per recorded point state, chronological: <code>sets</code>, per-set <code>games</code>,
in-game <code>points</code>, the <code>server</code>, the tiebreak flag, and the model's
<code>win_probability_p1</code> and <code>danger</code> at that exact point, each row timestamped.
<code>GET /history/matches/{matchId}</code> returns it per match (shape
<code>HistoryTape</code>: match metadata + tape + the model profiles produced during the
match); the monthly bulk packages ship the same per-point rows for every
completed match of the month.</p>
<h2 id="schemas">Schemas</h2>
<section id="schema-error">
<h3>Error</h3>
<div class="scrollx" tabindex="0" role="region" aria-label="Error fields"><table><caption class="vh">Error schema — fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead>
<tbody><tr><td><code>error</code></td><td>string</td><td></td></tr></tbody></table></div>
</section>
<section id="schema-listmeta">
<h3>ListMeta</h3>
<div class="scrollx" tabindex="0" role="region" aria-label="ListMeta fields"><table><caption class="vh">ListMeta schema — fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead>
<tbody><tr><td><code>limit</code></td><td>integer</td><td></td></tr>
<tr><td><code>offset</code></td><td>integer</td><td></td></tr>
<tr><td><code>count</code></td><td>integer</td><td></td></tr></tbody></table></div>
</section>
<section id="schema-score">
<h3>Score</h3>
<p>ULTRA adds win_probability_p1 + danger.</p>
<div class="scrollx" tabindex="0" role="region" aria-label="Score fields"><table><caption class="vh">Score schema — fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead>
<tbody><tr><td><code>sets</code></td><td>array of integer</td><td></td></tr>
<tr><td><code>games</code></td><td>array of array of integer</td><td>[games_p1, games_p2]; each a per-set list</td></tr>
<tr><td><code>points</code></td><td>array of string or null</td><td>In-game points as tennis strings ("0", "15", "40", "AD"). Entries can be NULL — observed live on completed matches, which also carry empty games arrays. Do not decode into non-nullable strings.</td></tr>
<tr><td><code>server</code></td><td>integer or null (1, 2, null)</td><td></td></tr>
<tr><td><code>is_tiebreak</code></td><td>boolean</td><td></td></tr>
<tr><td><code>win_probability_p1</code></td><td>number or null</td><td></td></tr>
<tr><td><code>danger</code></td><td>number or null</td><td></td></tr>
<tr><td><code>timestamp</code></td><td>string or null</td><td></td></tr></tbody></table></div>
</section>
<section id="schema-player">
<h3>Player</h3>
<div class="scrollx" tabindex="0" role="region" aria-label="Player fields"><table><caption class="vh">Player schema — fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead>
<tbody><tr><td><code>id</code></td><td>integer</td><td></td></tr>
<tr><td><code>name</code></td><td>string</td><td></td></tr>
<tr><td><code>tour</code></td><td>string or null</td><td>The record's OWN tour, which is NOT the <code>tour</code> filter vocabulary. It is granular (<code>juniors_boys</code>, <code>juniors_girls</code>, <code>challenger_men</code>) where the filter is grouped (<code>juniors</code>, <code>challenger</code>), and a doubles team reports it UPPERCASE (<code>ATP</code>) where an individual reports lowercase (<code>atp</code>). Treat it as an opaque string; do not parse it into the filter enum.</td></tr>
<tr><td><code>country</code></td><td>string or null</td><td></td></tr>
<tr><td><code>ranking</code></td><td>integer or null</td><td></td></tr>
<tr><td><code>ranking_points</code></td><td>integer or null</td><td></td></tr>
<tr><td><code>ranking_movement</code></td><td>string or null (up, down, same, null)</td><td></td></tr>
<tr><td><code>hand</code></td><td>string or null (R, L, null)</td><td></td></tr>
<tr><td><code>backhand</code></td><td>integer or null (1, 2, null)</td><td></td></tr>
<tr><td><code>birthday</code></td><td>string or null</td><td></td></tr>
<tr><td><code>is_doubles_team</code></td><td>boolean</td><td></td></tr>
<tr><td><code>data_completeness</code></td><td>object</td><td>How much biographical detail is known for this player, so a consumer can distinguish "not in the feed" from "not yet fetched" without probing. Present on every player in a match payload. Lower tours carry far less of it than main tour.</td></tr>
<tr><td><code>stats</code></td><td>object</td><td>Single-player endpoint only</td></tr></tbody></table></div>
</section>
<section id="schema-match">
<h3>Match</h3>
<div class="scrollx" tabindex="0" role="region" aria-label="Match fields"><table><caption class="vh">Match schema — fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead>
<tbody><tr><td><code>id</code></td><td>integer</td><td></td></tr>
<tr><td><code>tournament</code></td><td>string</td><td></td></tr>
<tr><td><code>surface</code></td><td>string or null (hard, clay, grass, null)</td><td></td></tr>
<tr><td><code>indoor</code></td><td>boolean</td><td></td></tr>
<tr><td><code>format</code></td><td>string or null (BO3, BO5, null)</td><td></td></tr>
<tr><td><code>round</code></td><td>string or null</td><td></td></tr>
<tr><td><code>status</code></td><td>string (upcoming, live, completed, cancelled)</td><td></td></tr>
<tr><td><code>event_status</code></td><td>string or null</td><td></td></tr>
<tr><td><code>is_doubles</code></td><td>boolean</td><td></td></tr>
<tr><td><code>scheduled_time</code></td><td>string or null</td><td></td></tr>
<tr><td><code>players</code></td><td>object</td><td></td></tr>
<tr><td><code>score</code></td><td>object or null</td><td></td></tr>
<tr><td><code>winner</code></td><td>integer or null</td><td>Completed matches only — derived from final sets</td></tr></tbody></table></div>
</section>
<section id="schema-matchdetail">
<h3>MatchDetail</h3>
<div class="scrollx" tabindex="0" role="region" aria-label="MatchDetail fields"><table><caption class="vh">MatchDetail schema — fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead>
<tbody><tr><td><code>id</code></td><td>integer</td><td></td></tr>
<tr><td><code>tournament</code></td><td>string</td><td></td></tr>
<tr><td><code>surface</code></td><td>string or null (hard, clay, grass, null)</td><td></td></tr>
<tr><td><code>indoor</code></td><td>boolean</td><td></td></tr>
<tr><td><code>format</code></td><td>string or null (BO3, BO5, null)</td><td></td></tr>
<tr><td><code>round</code></td><td>string or null</td><td></td></tr>
<tr><td><code>status</code></td><td>string (upcoming, live, completed, cancelled)</td><td></td></tr>
<tr><td><code>event_status</code></td><td>string or null</td><td></td></tr>
<tr><td><code>is_doubles</code></td><td>boolean</td><td></td></tr>
<tr><td><code>scheduled_time</code></td><td>string or null</td><td></td></tr>
<tr><td><code>players</code></td><td>object</td><td></td></tr>
<tr><td><code>score</code></td><td>object or null</td><td></td></tr>
<tr><td><code>winner</code></td><td>integer or null</td><td>Completed matches only — derived from final sets</td></tr>
<tr><td><code>analysis</code></td><td>object</td><td>ULTRA only (absent below)</td></tr>
<tr><td><code>market</code></td><td>object or null</td><td>PRO+ only (absent below)</td></tr></tbody></table></div>
</section>
<section id="schema-analysis">
<h3>Analysis</h3>
<div class="scrollx" tabindex="0" role="region" aria-label="Analysis fields"><table><caption class="vh">Analysis schema — fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead>
<tbody><tr><td><code>thesis</code></td><td>object or null</td><td></td></tr>
<tr><td><code>profile</code></td><td>object or null</td><td></td></tr></tbody></table></div>
</section>
<section id="schema-market">
<h3>Market</h3>
<div class="scrollx" tabindex="0" role="region" aria-label="Market fields"><table><caption class="vh">Market schema — fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead>
<tbody><tr><td><code>id</code></td><td>integer</td><td></td></tr>
<tr><td><code>question</code></td><td>string or null</td><td></td></tr>
<tr><td><code>status</code></td><td>string or null (active, resolved, closed, null)</td><td></td></tr>
<tr><td><code>volume</code></td><td>number or null</td><td></td></tr>
<tr><td><code>liquidity</code></td><td>number or null</td><td></td></tr>
<tr><td><code>end_date</code></td><td>string or null</td><td></td></tr>
<tr><td><code>prices</code></td><td>array of object</td><td>Prices endpoint / match detail only; newest first</td></tr></tbody></table></div>
</section>
<section id="schema-price">
<h3>Price</h3>
<div class="scrollx" tabindex="0" role="region" aria-label="Price fields"><table><caption class="vh">Price schema — fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead>
<tbody><tr><td><code>side</code></td><td>integer or null</td><td>1 = p1's outcome, 2 = p2's</td></tr>
<tr><td><code>bid</code></td><td>number or null</td><td></td></tr>
<tr><td><code>ask</code></td><td>number or null</td><td></td></tr>
<tr><td><code>mid</code></td><td>number or null</td><td></td></tr>
<tr><td><code>spread</code></td><td>number or null</td><td></td></tr>
<tr><td><code>timestamp</code></td><td>string or null</td><td></td></tr></tbody></table></div>
</section>
<section id="schema-event">
<h3>Event</h3>
<div class="scrollx" tabindex="0" role="region" aria-label="Event fields"><table><caption class="vh">Event schema — fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead>
<tbody><tr><td><code>type</code></td><td>string (break, set_won, game_won, momentum_run)</td><td></td></tr>
<tr><td><code>player</code></td><td>integer or null (1, 2, null)</td><td></td></tr>
<tr><td><code>timestamp</code></td><td>string or null</td><td></td></tr></tbody></table></div>
</section>
<section id="schema-fixture">
<h3>Fixture</h3>
<div class="scrollx" tabindex="0" role="region" aria-label="Fixture fields"><table><caption class="vh">Fixture schema — fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead>
<tbody><tr><td><code>id</code></td><td>integer</td><td></td></tr>
<tr><td><code>event_date</code></td><td>string or null</td><td></td></tr>
<tr><td><code>tour</code></td><td>string or null</td><td>The record's OWN tour, which is NOT the <code>tour</code> filter vocabulary. It is granular (<code>juniors_boys</code>, <code>juniors_girls</code>, <code>challenger_men</code>) where the filter is grouped (<code>juniors</code>, <code>challenger</code>), and a doubles team reports it UPPERCASE (<code>ATP</code>) where an individual reports lowercase (<code>atp</code>). Treat it as an opaque string; do not parse it into the filter enum.</td></tr>
<tr><td><code>tournament</code></td><td>string or null</td><td></td></tr>
<tr><td><code>round</code></td><td>string or null</td><td></td></tr>
<tr><td><code>surface</code></td><td>string or null</td><td></td></tr>
<tr><td><code>player1_name</code></td><td>string or null</td><td></td></tr>
<tr><td><code>player2_name</code></td><td>string or null</td><td></td></tr>
<tr><td><code>status</code></td><td>string or null</td><td></td></tr></tbody></table></div>
</section>
<section id="schema-modelprofile">
<h3>ModelProfile</h3>
<p>One model win-probability profile. On the history tape these are the profiles produced during the match, oldest first.</p>
<div class="scrollx" tabindex="0" role="region" aria-label="ModelProfile fields"><table><caption class="vh">ModelProfile schema — fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead>
<tbody><tr><td><code>win_probability_p1</code></td><td>number or null</td><td></td></tr>
<tr><td><code>expected_closeness</code></td><td>number or null</td><td></td></tr>
<tr><td><code>volatility_rating</code></td><td>string or null (low, med, high, null)</td><td></td></tr>
<tr><td><code>key_factors</code></td><td>array or null</td><td></td></tr>
<tr><td><code>created_at</code></td><td>string or null</td><td></td></tr></tbody></table></div>
</section>
<section id="schema-historytape">
<h3>HistoryTape</h3>
<p>The full per-match history tape (BASIC, or Historical Data API Starter+). <code>tape</code> rows are Score objects and INCLUDE the model fields <code>win_probability_p1</code> and <code>danger</code> per point — on this product they are part of the tape itself, not an ULTRA upsell.</p>
<div class="scrollx" tabindex="0" role="region" aria-label="HistoryTape fields"><table><caption class="vh">HistoryTape schema — fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead>
<tbody><tr><td><code>match</code></td><td>object</td><td>Match metadata with the final score embedded.</td></tr>
<tr><td><code>tape</code></td><td>array of object</td><td>Chronological score rows, one per recorded point state.</td></tr>
<tr><td><code>profiles</code></td><td>array of object</td><td>Model profiles produced during the match, oldest first.</td></tr>
<tr><td><code>meta</code></td><td>object</td><td></td></tr></tbody></table></div>
</section>
<section id="schema-historypackage">
<h3>HistoryPackage</h3>
<p>Manifest of one pre-built monthly bulk package (PRO, Historical Data API Pro+, or a one-off package pass).</p>
<div class="scrollx" tabindex="0" role="region" aria-label="HistoryPackage fields"><table><caption class="vh">HistoryPackage schema — fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead>
<tbody><tr><td><code>period</code></td><td>string</td><td>Calendar month, <code>YYYY-MM</code>.</td></tr>
<tr><td><code>status</code></td><td>string (ready)</td><td></td></tr>
<tr><td><code>match_count</code></td><td>integer</td><td></td></tr>
<tr><td><code>row_count</code></td><td>integer</td><td></td></tr>
<tr><td><code>files</code></td><td>array of object</td><td>One entry per downloadable format.</td></tr>
<tr><td><code>built_at</code></td><td>string or null</td><td></td></tr></tbody></table></div>
</section>
<section id="schema-breakpoint">
<h3>BreakPoint</h3>
<p>WebSocket <code>break_point</code> frame (ULTRA). Pushed the instant a break point arises, but only when the subscription named <code>break_point</code> in its <code>signals</code>. Fields sit inline (no nested score object); <code>prob_swing</code> mirrors the REST score's <code>danger</code>. <code>server</code> is the player serving, <code>returner</code> the one holding the break point(s).</p>
<div class="scrollx" tabindex="0" role="region" aria-label="BreakPoint fields"><table><caption class="vh">BreakPoint schema — fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead>
<tbody><tr><td><code>type</code></td><td>string</td><td></td></tr>
<tr><td><code>match_id</code></td><td>integer</td><td></td></tr>
<tr><td><code>server</code></td><td>integer or null (1, 2, null)</td><td></td></tr>
<tr><td><code>returner</code></td><td>integer or null (1, 2, null)</td><td></td></tr>
<tr><td><code>break_points</code></td><td>integer or null</td><td>How many break points are live at once (1-3).</td></tr>
<tr><td><code>set</code></td><td>integer or null</td><td></td></tr>
<tr><td><code>game</code></td><td>integer or null</td><td></td></tr>
<tr><td><code>point</code></td><td>string or null</td><td></td></tr>
<tr><td><code>win_probability_p1</code></td><td>number or null</td><td></td></tr>
<tr><td><code>prob_swing</code></td><td>number or null</td><td></td></tr>
<tr><td><code>server_side_favoured</code></td><td>boolean or null</td><td></td></tr>
<tr><td><code>ts</code></td><td>string or null</td><td></td></tr></tbody></table></div>
</section>
<section id="schema-breakpointresult">
<h3>BreakPointResult</h3>
<p>WebSocket <code>break_point_result</code> frame (ULTRA): a break point just resolved. <code>outcome</code> is <code>held</code> (server saved it) or <code>broken</code> (returner converted); <code>win_probability_p1_after</code> is p1's win probability once the game closed.</p>
<div class="scrollx" tabindex="0" role="region" aria-label="BreakPointResult fields"><table><caption class="vh">BreakPointResult schema — fields</caption><thead><tr><th scope="col">Field</th><th scope="col">Type</th><th scope="col">Description</th></tr></thead>
<tbody><tr><td><code>type</code></td><td>string</td><td></td></tr>
<tr><td><code>match_id</code></td><td>integer</td><td></td></tr>
<tr><td><code>server</code></td><td>integer or null (1, 2, null)</td><td></td></tr>
<tr><td><code>outcome</code></td><td>string (held, broken)</td><td></td></tr>
<tr><td><code>win_probability_p1_after</code></td><td>number or null</td><td></td></tr>
<tr><td><code>ts</code></td><td>string or null</td><td></td></tr></tbody></table></div>
</section>
</main>
<footer>
<hr>
<p class="meta">Generated from <a href="./openapi.yaml">openapi.yaml</a> by
<a href="https://github.com/livetennisapi/openapi">livetennisapi/openapi</a>.</p>
<nav class="footnav" aria-label="Elsewhere">
<a href="https://github.com/livetennisapi/openapi/issues">Report a spec mismatch</a>
<a href="https://affiliates.livetennisapi.com/program">Affiliate programme — 51% lifetime</a>
</nav>
<p class="meta">Writing about tennis, or building a tool on this API? The affiliate
programme pays 51% recurring for the lifetime of every subscription referred,
10% off for them, free to join.</p>
</footer>
</div>
</body>
</html>