Repository navigation
Expand file tree
/
Copy pathindex.html
More file actions
609 lines (564 loc) · 35.7 KB
/
Copy pathindex.html
File metadata and controls
609 lines (564 loc) · 35.7 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
<!doctype html>
<html lang="en">
<head>
<!-- Scalar's bundle comes from jsDelivr, so the browser cannot start fetching it until it has
done DNS, TCP and TLS to a host it has never spoken to. Warming that connection in the head
overlaps it with parsing the page instead of queueing behind it. Free, and it changes
nothing about what loads — see the integrity-pinned script tag at the bottom of the body.
It does NOT fix the page's real problem: PageSpeed puts this page at performance 30 and
LCP 8.94s on throttled mobile, because Scalar's own render is the largest paint and it
happens late. That is inherent to a client-rendered reference, not a tuning matter, and
the structural options are recorded in tracker task 11.12. The CONTENT is not at risk
either way: the server-rendered intro above, reference.html and the eight topic pages
carry it all in raw HTML, which is what answer engines read. -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Live Tennis API — API Reference</title>
<meta name="description" content="API reference for Live Tennis API: live scores, players, rankings, match-winner prices and model win-probability for ATP, WTA, Challenger and ITF.">
<link rel="icon" href="favicon.ico" sizes="any">
<link rel="icon" href="logo.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="icon-256.png">
<link rel="canonical" href="https://docs.livetennisapi.com/">
<link rel="alternate" type="text/markdown" href="https://docs.livetennisapi.com/llms.txt" title="API reference (Markdown digest)">
<link rel="alternate" type="application/json" href="https://docs.livetennisapi.com/openapi.json" title="OpenAPI 3.1 (JSON)">
<link rel="alternate" type="application/yaml" href="https://docs.livetennisapi.com/openapi.yaml" title="OpenAPI 3.1 (YAML)">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Live Tennis API">
<meta property="og:title" content="Live Tennis API — API Reference">
<meta property="og:description" content="Real-time tennis scores, players, rankings, match-winner market prices and model win-probability over REST and WebSocket.">
<meta property="og:url" content="https://docs.livetennisapi.com/">
<meta property="og:image" content="https://docs.livetennisapi.com/banner.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Live Tennis API — API Reference">
<meta name="twitter:description" content="Real-time tennis scores, players, rankings, market prices and model win-probability over REST and WebSocket.">
<meta name="twitter:image" content="https://docs.livetennisapi.com/banner.jpg">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"name": "Live Tennis API — API Reference",
"headline": "Live Tennis API — API Reference",
"description": "API reference for Live Tennis API: real-time tennis scores, players, rankings, match-winner market prices and model win-probability over REST and WebSocket.",
"url": "https://docs.livetennisapi.com/",
"inLanguage": "en",
"isPartOf": { "@type": "WebSite", "name": "Live Tennis API", "url": "https://livetennisapi.com" },
"publisher": {
"@type": "Organization",
"@id": "https://livetennisapi.com/#org",
"name": "JSB Holdings LLC",
"alternateName": "Live Tennis API",
"url": "https://livetennisapi.com",
"logo": "https://docs.livetennisapi.com/icon-256.png"
}
}
</script>
<!-- Self-hosted webfonts. Nothing here is fetched from a third party, so the
page has no font dependency it cannot verify. -->
<link rel="preload" href="fonts/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="fonts/space-grotesk-latin-700.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="fonts.css">
<style>
/* ---------------------------------------------------------------- tokens
The product's design tokens (app/services/design_tokens.py). Every colour
and radius below resolves to one of these; no page-local palette. */
:root {
--bg: #0e0e0e;
--panel: #141414;
--struct: #2a2a2a;
--text: #e4e2e1;
--muted: #84967e;
--ours: #00ff41;
--r: 6px; /* control radius; structural elements are square */
--docsbar: 52px; /* height of the site nav bar, reserved below */
color-scheme: dark;
}
html, body { margin: 0; padding: 0; background: var(--bg); }
body { font-family: 'Inter', ui-sans-serif, system-ui, sans-serif; }
/* ------------------------------------------------------- the site nav bar
WHAT THIS REPLACES. These three links used to be fixed pills in the
bottom-right corner. Measured at 1440x900, they sat on top of four live
Scalar controls — at scrollY=1600 `elementFromPoint` returned #quickref,
#afflink or #textref over the copy button, "Test Request", "Send Request"
and "Show Schema". Below 720px a comment claimed they dropped BELOW the
content; they actually rendered as a 137px band at the top of the very
first viewport (16% of it).
So: they are a real nav bar now, at the top, at every width, and it sits
IN NORMAL FLOW rather than floating.
In flow, specifically, and not `position: fixed`. A fixed bar was tried
first and simply moved the problem: Scalar's own chrome is sticky and
fixed too, so at z-index 60 the bar covered the mobile "Open Menu"
hamburger, the API client's "Close Client" button and the sticky "cURL /
Copy" controls — nine overlaps at 1440x900, eight at 390x844. Occupying
real space is the only version where nothing can overlap anything,
because there is no stacking contest to win. The bar scrolls away with
the page, exactly like a site header. */
.docs-corner {
position: relative; z-index: 2; /* above the boot splash only */
display: flex; align-items: center; gap: 8px;
height: var(--docsbar); padding: 0 12px;
background: var(--bg); border-bottom: 1px solid var(--struct);
overflow-x: auto; overflow-y: hidden; /* never widens the page */
scrollbar-width: none;
}
.docs-corner::-webkit-scrollbar { display: none; }
#textref, #quickref, #afflink {
flex: 0 0 auto;
display: inline-flex; align-items: center; min-height: 44px;
background: var(--panel); color: var(--ours); border: 1px solid var(--struct);
border-radius: var(--r); padding: 0 14px; text-decoration: none;
font: 500 13px/1 'Inter', ui-sans-serif, system-ui, sans-serif;
white-space: nowrap;
}
#afflink { color: var(--muted); }
#textref:hover, #quickref:hover { border-color: var(--ours); }
#afflink:hover { border-color: var(--ours); color: var(--ours); }
#textref:focus-visible, #quickref:focus-visible, #afflink:focus-visible {
outline: 2px solid var(--ours); outline-offset: 2px;
}
/* Shorter labels where the bar is narrow, so all three fit without scrolling. */
.lbl-short { display: none; }
@media (max-width: 720px) {
.lbl-full { display: none; }
.lbl-short { display: inline; }
#textref, #quickref, #afflink { padding: 0 11px; }
}
/* Pre-hydration splash so the page never flashes white on a slow load. */
#boot {
position: fixed; inset: var(--docsbar) 0 0 0; display: grid; place-items: center;
background: var(--bg); color: var(--muted); z-index: 1;
font: 500 14px/1.5 'Inter', ui-sans-serif, system-ui, sans-serif;
text-align: center;
}
#boot .dot {
width: 10px; height: 10px; border-radius: 50%; background: var(--ours);
margin: 0 auto 12px; animation: p 1.1s ease-in-out infinite;
}
@keyframes p { 0%,100% { opacity: .25; transform: scale(.85); } 50% { opacity: 1; transform: scale(1); } }
@media (prefers-reduced-motion: reduce) { #boot .dot { animation: none; opacity: .8; } }
/* Fallback surfaces: the noscript block and the CDN-failure message. These
are exactly what a visitor sees when something is broken, so they use the
same tokens as everything else rather than a stale palette of their own. */
.fallback { max-width: 820px; margin: 0 auto; padding: 48px 20px;
color: var(--text); font: 16px/1.6 'Inter', ui-sans-serif, system-ui, sans-serif; }
.fallback h1 { color: var(--ours); font-family: 'Space Grotesk', 'Inter', sans-serif; }
/* The intro is a crawler surface, not a layout participant. It was IN FLOW until 2026-09-14
and hiding it when Scalar painted moved everything below it — Lighthouse measured CLS 0.82
on mobile, worse than the 32-word page it replaced. Out of flow (fixed, same box as #boot,
above the splash) it can be removed at any moment for zero shift; a non-rendering crawler
still reads it because it is ordinary markup in the document. */
#static-intro {
position: fixed; inset: var(--docsbar) 0 auto 0; z-index: 2;
background: var(--bg); max-height: calc(100dvh - var(--docsbar)); overflow-y: auto;
}
#static-intro[hidden] { display: none; }
/* The explorer button reads as the primary action, since the page is now the docs and the
explorer is the opt-in. Sized to the 44px touch target the Bearer input already uses. */
#open-explorer {
font: inherit; color: var(--bg); background: var(--ours); border: 0;
border-radius: var(--r); padding: 11px 18px; min-height: 44px; cursor: pointer;
}
#open-explorer:hover { filter: brightness(1.08); }
#open-explorer:disabled { opacity: .7; cursor: progress; }
#open-explorer:focus-visible { outline: 2px solid var(--ours); outline-offset: 2px; }
.fallback a, .boot-fail a { color: var(--ours); }
.fallback .lead { font-size: 1.1rem; }
.fallback .dim, .boot-fail .dim { color: var(--muted); }
.boot-fail { max-width: 44ch; color: var(--text); }
.boot-fail .lead { font-size: 1.05rem; }
.boot-fail .small { font-size: .9rem; }
/* ------------------------------------------------- Scalar theme overrides
Scalar ships with `theme: "purple"`, which is not this product's brand and
failed contrast in two places measured on its own surfaces: the `curl`
keyword at 3.09:1 and the GET badge / JSON strings at 3.68:1, both under
the 4.5:1 floor for text below 18px.
So the vendor theme is switched OFF (`theme: "none"`) and rebuilt on the
design tokens here.
SPECIFICITY, THE LOAD-BEARING DETAIL: Scalar puts `.dark-mode` on <body>
and defines its variables there. A plain `:root` block (0,0,1) LOSES to
`.dark-mode` (0,1,0) and silently does nothing. `body.dark-mode` (0,1,1)
wins, which is why the selector list below is written the way it is. */
:root,
body.dark-mode {
/* surfaces */
--scalar-background-1: var(--bg);
--scalar-background-2: var(--panel);
--scalar-background-3: var(--struct);
--scalar-background-accent: var(--panel);
--scalar-background-alert: var(--panel);
--scalar-background-danger: var(--panel);
--scalar-header-background-1: var(--bg);
--scalar-sidebar-background-1: var(--bg);
--scalar-sidebar-search-background: var(--panel);
/* structure — hairlines, square corners */
--scalar-border-color: var(--struct);
--scalar-border-width: 1px;
--scalar-sidebar-border-color: var(--struct);
--scalar-sidebar-search-border-color: var(--struct);
/* text. --scalar-color-3 is deliberately the same as -2: the recede tier
(#5a6675) is 3.30:1 on the page background, and Scalar uses colour-3 for
small labels where the floor is 4.5:1. Collapsing a tier beats shipping
unreadable text. */
--scalar-color-1: var(--text);
--scalar-color-2: var(--muted);
--scalar-color-3: var(--muted);
--scalar-color-accent: var(--ours);
--scalar-color-disabled: var(--muted);
--scalar-sidebar-color-1: var(--text);
--scalar-sidebar-color-2: var(--muted);
--scalar-sidebar-color-active: var(--ours);
--scalar-sidebar-item-hover-color: var(--text);
--scalar-sidebar-item-hover-background: var(--panel);
--scalar-sidebar-item-active-background: var(--panel);
--scalar-sidebar-search-color: var(--muted);
/* links */
--scalar-link-color: var(--ours);
--scalar-link-color-hover: var(--ours);
--scalar-text-decoration: none;
--scalar-text-decoration-hover: underline;
/* semantic + syntax highlighting. Every one of these is >= 4.5:1 on all
three surfaces above (measured, see the commit message). The two that
were failing keep their hue and only gain lightness:
purple #7a59b1 -> #b191f9 3.09:1 -> 7.27:1 on --panel
blue #2b7abf -> #6bc1fe 3.68:1 -> 9.37:1 on --panel */
--scalar-color-green: var(--ours);
--scalar-color-blue: #6bc1fe;
--scalar-color-purple: #b191f9;
--scalar-color-orange: #ffb000;
--scalar-color-yellow: #ffb000;
--scalar-color-red: #ff8589;
--scalar-color-alert: #ffb000;
--scalar-color-danger: #ff8589;
/* buttons */
--scalar-button-1: var(--ours);
--scalar-button-1-color: var(--bg);
--scalar-button-1-hover: var(--ours);
/* radii: 0 structural, 6px control */
--scalar-radius: 6px;
--scalar-radius-md: 6px;
--scalar-radius-lg: 0;
--scalar-radius-xl: 0;
--scalar-radius-2xl: 0;
--scalar-radius-3xl: 0;
--scalar-radius-max: 0;
/* type */
--scalar-font: 'Inter', ui-sans-serif, system-ui, sans-serif;
--scalar-font-code: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
/* the system is flat: no shadows, no glows, no backdrop tricks */
--scalar-shadow-1: none;
--scalar-shadow-2: none;
--scalar-lifted-brightness: 1;
--scalar-backdrop-brightness: 1;
/* chrome */
--scalar-scrollbar-color: var(--struct);
--scalar-scrollbar-color-active: var(--muted);
--scalar-tooltip-background: var(--panel);
--scalar-tooltip-color: var(--text);
}
/* Headings take the display face, as everywhere else in the product. */
.scalar-app h1, .scalar-app h2, .scalar-app h3 {
font-family: 'Space Grotesk', 'Inter', ui-sans-serif, sans-serif;
}
/* Touch targets to the 44px floor, everywhere CSS can reach.
Scalar renders its sidebar navigation at 32px and 48px; its icon buttons
(the mobile "Open Menu" hamburger, "Show Password") at 32px and 24px; and
its tab / disclosure / popover chrome at 32-33px. All of those are under
the floor on every device, and all of them are plain boxes that grow
safely. */
aside a, aside button { min-height: 44px; }
.scalar-app .download-button { min-height: 44px; }
.scalar-app .scalar-icon-button { min-width: 44px; min-height: 44px; }
.scalar-app [id^="headlessui-tabs-tab"],
.scalar-app [id^="headlessui-disclosure-button"],
.scalar-app [id^="headlessui-popover-button"] { min-height: 44px; }
.scalar-app .show-api-client-button { min-height: 44px; }
.scalar-app .section-header-wrapper a { min-height: 44px; }
/* The credential field. Its wrapper carries Tailwind `max-h-8`, which caps
the row at 32px, so raising the input alone does nothing — the cap has to
go up with it. This is the Bearer token input; of everything on the page
it is the one control that should not be fiddly to hit. */
.scalar-app input { min-height: 44px; }
.scalar-app .max-h-8:has(input) { max-height: 44px; min-height: 44px; }
/* Remove the vendor's own attribution badge. Scalar is MIT, so this is
permitted; the licence text inside the bundle is untouched.
THE TRAP: `a[href*="scalar.com"]` also matches the two legitimate
"Open API Client" links and Scalar's Terms / Privacy links, and would
delete working features. The badge is the one that carries
`.no-underline`, so the selector is anchored on that. */
.scalar-app a.no-underline[href*="scalar.com"] { display: none !important; }
/* Keep the language menu adjacent to the tabs, outside their tablist. The
original buttons and popover stay intact so Scalar owns their behaviour. */
.scalar-app .docs-client-library-tabs { display: flex; align-items: stretch; }
.scalar-app .docs-client-library-tabs > .client-libraries-content {
flex: 1; min-width: 0; border-right: 0;
}
.scalar-app .docs-client-library-tabs > .contents > .client-libraries__select {
width: auto; min-width: 60px; padding: 8px 10px;
border-right: 1px solid var(--scalar-border-color);
}
.docs-home .scalar-app[hidden] { display: none; }
</style>
<link rel="stylesheet" href="docs-design-20260918.css">
</head>
<body class="docs-home">
<a class="skip-link" href="#static-intro">Skip to content</a>
<header class="docs-site-header"><a class="docs-brand" href="./"><img src="logo.svg" width="28" height="28" alt="">Live Tennis API <span>Docs</span></a><nav class="docs-desktop-nav" aria-label="Documentation"><a href="./" aria-current="page">Overview</a><a href="./reference.html">API reference</a><a href="./changelog.html">Changelog</a></nav><a class="docs-key" href="https://livetennisapi.com/subscribe/free">Get a free key <span aria-hidden="true">↗</span></a></header><details class="docs-topic-menu"><summary>Explore the documentation</summary><nav aria-label="Documentation topics"><a href="./" aria-current="page">Overview</a><a href="./reference.html">API reference</a><a href="./changelog.html">Changelog</a><a href="./live-scores.html">Live tennis scores API</a><a href="./players-and-tournaments.html">Tennis players, tournaments and rankings API</a><a href="./tennis-odds.html">Tennis odds API — markets and price ticks</a><a href="./point-by-point-history.html">Point-by-point tennis data API</a><a href="./historical-results-archive.html">Historical tennis results API — 1968 onward</a><a href="./shot-level-rally-data.html">Shot-by-shot tennis rally and charting API</a><a href="./broadcast-graphics.html">Tennis scores for on-air graphics</a><a href="./push-feed-and-webhooks.html">Tennis WebSocket feed and webhooks</a><a href="./auth-quota-and-health.html">Tennis API authentication, quota and status</a></nav></details>
<!-- No boot splash any more: nothing loads until asked, so there is nothing to wait for.
The div is kept (empty, hidden) because the on-demand loader reuses it as the mount
point and the failure surface. -->
<div id="boot" hidden></div>
<!-- Server-rendered intro (SEO/AEO audit 2026-09-14). Measured before this existed: the raw
HTML of this page carried 32 visible words and not even the API base URL; everything
else arrived with Scalar. Answer engines fetch, they do not render, and most of them skip the
noscript fallback too — so the essentials sit here in plain markup and the boot script hides the
block the moment Scalar paints. Same text as the noscript fallback below, kept in sync. -->
<main id="static-intro" class="fallback">
<div class="docs-hero">
<div>
<p class="docs-eyebrow">Developer documentation / v1</p>
<h1>Tennis data.<br>Start building.</h1>
<p class="docs-lede">From your first live score to a complete match experience. Explore players, rankings, results, point-by-point history, market prices and model probabilities over REST and WebSocket.</p>
<div class="docs-actions"><button id="open-explorer" type="button">Open the interactive explorer <span aria-hidden="true">↗</span></button><a href="reference.html#quickstart">Read the quickstart →</a></div>
<p class="docs-lede" style="font-size:12px">ATP · WTA · Challenger · ITF. <a href="https://livetennisapi.com/subscribe/free">Get a free key</a>, no card required.</p>
</div>
<div class="docs-code">
<div class="docs-code-label"><span>Your first request</span><span>cURL / REST</span></div>
<pre><span class="code-accent">curl</span> --get \
https://api.livetennisapi.com/api/public/v1/matches \
--data-urlencode 'status=live' \
--data-urlencode 'limit=5' \
--header 'X-API-Key: YOUR_API_KEY'</pre>
<p>Replace the placeholder with your key. A successful response may have an empty <code>data</code> array when no match is live. One request uses one call from your quota.</p>
</div>
</div>
<div class="docs-section-title"><h2>Find the data you need.</h2><p>Endpoints, parameters and response examples, by topic.</p></div>
<nav class="docs-topics" aria-label="API topics">
<a href="live-scores.html"><small>01 / LIVE</small><strong>Scores & match state</strong><span>Sets, games, points and score changes.</span></a>
<a href="players-and-tournaments.html"><small>02 / DIRECTORY</small><strong>Players & tournaments</strong><span>Player profiles, rankings and competition details.</span></a>
<a href="tennis-odds.html"><small>03 / MARKETS</small><strong>Odds & price ticks</strong><span>Match-winner markets and their recorded updates.</span></a>
<a href="point-by-point-history.html"><small>04 / HISTORY</small><strong>Point-by-point data</strong><span>Inspect match sequences and event history.</span></a>
<a href="historical-results-archive.html"><small>05 / ARCHIVE</small><strong>Historical results</strong><span>Results archive from 1968 through 2022.</span></a>
<a href="shot-level-rally-data.html"><small>06 / DETAIL</small><strong>Shot & rally data</strong><span>Charted shots and rally-level detail where covered.</span></a>
<a href="broadcast-graphics.html"><small>07 / BROADCAST</small><strong>On-air graphics</strong><span>One flat object per match, built for a graphics template at 1 Hz.</span></a>
<a href="push-feed-and-webhooks.html"><small>08 / STREAMING</small><strong>WebSocket & webhooks</strong><span>Connect to push updates and delivery endpoints.</span></a>
<a href="auth-quota-and-health.html"><small>09 / ESSENTIALS</small><strong>Auth, quota & status</strong><span>Authenticate, understand limits and check health.</span></a>
</nav>
<nav class="docs-resources" aria-label="Reference resources"><a href="reference.html">Complete HTML reference</a><a href="llms.txt">Markdown / llms.txt</a><a href="openapi.yaml">OpenAPI YAML</a><a href="openapi.json">OpenAPI JSON</a><a href="changelog.html">Changelog</a><a href="https://livetennisapi.com/pricing">Compare plans</a></nav>
<div class="docs-clients"><span>Official clients</span><code>pip install livetennisapi</code><code>npm install livetennisapi</code><code>npx livetennisapi-mcp</code></div>
</main>
<!-- The interactive reference renders client-side. No major AI crawler executes
JavaScript (GPTBot, ClaudeBot, PerplexityBot, OAI-SearchBot all read raw HTML
only), so without this the entire API reference is invisible to answer engines.
reference.html carries the same content as plain server-rendered HTML. -->
<noscript><style>#open-explorer{display:none}</style><p class="fallback">The interactive explorer needs JavaScript. All <a href="reference.html">reference pages and examples</a> remain available.</p></noscript>
<nav class="docs-corner" aria-label="Documentation">
<a href="reference.html#quickstart" id="quickref"><span class="lbl-full">Quickstart (no code)</span><span class="lbl-short">Quickstart</span></a>
<a href="reference.html" id="textref"><span class="lbl-full">Text reference (no JS)</span><span class="lbl-short">Text reference</span></a>
<a href="https://affiliates.livetennisapi.com/program" id="afflink"
target="_blank" rel="noopener"><span class="lbl-full">Affiliate programme — 51% lifetime</span><span class="lbl-short">Affiliates — 51%</span></a>
</nav>
<script
id="api-reference"
data-url="openapi.yaml"
data-configuration='{"theme":"none","darkMode":true,"forceDarkModeState":"dark","hideDarkModeToggle":true,"withDefaultFonts":false,"hideDownloadButton":false,"searchHotKey":"k","metaData":{"title":"Live Tennis API — API Reference"}}'>
</script>
<!-- PINNED AND HASHED, deliberately.
This is 3.6 MB of third-party JavaScript executing on a page with a Bearer
token input. It used to load from the unversioned
`cdn.jsdelivr.net/npm/@scalar/api-reference`, which resolves to whatever
the maintainers published most recently — a silent, unreviewed code change
on a credential-handling page.
The URL below is the exact npm artifact for 1.63.0; its bytes were verified
against the registry tarball before the hash was taken, and the integrity
attribute pins them. Note it points at `dist/browser/standalone.js` rather
than the bare package path: the bare path serves a file jsDelivr minifies
on the fly, whose own header warns "Do NOT use SRI with dynamically
generated files" because it changes whenever their Terser does.
Upgrading = change the version, re-download that exact URL, and recompute:
openssl dgst -sha384 -binary <file> | openssl base64 -A -->
<script>
// Scalar loads ON DEMAND, not on page load.
//
// WHY. PageSpeed measured this page at performance 32 with an 8.86s LCP on throttled mobile,
// the only page on any host failing Core Web Vitals. The cause is structural rather than a
// tuning problem: Scalar's bundle renders a very large element late, and LCP tracks the
// LARGEST contentful paint, so it keeps updating until that arrives. Deferring the script or
// warming the CDN connection cannot fix it — only not painting it can.
//
// Everything the page needs to BE the documentation is already server-rendered above: the
// base URL, the auth header, the free-key link, and links to the full text reference and the
// eight per-topic pages. reference.html scores 97 and the topic pages 96. So the fast,
// crawlable page is the default and the explorer is one click away.
//
// The integrity hash and crossorigin attribute are carried onto the injected element, so the
// subresource-integrity guarantee is exactly what it was when the tag was static. Upgrading is
// still: change the version, re-download that exact URL, recompute the hash.
(function () {
var SRC = 'https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.63.0/dist/browser/standalone.js';
var SRI = 'sha384-PlviHlK3uCjxgh67enSuQJvyxdVZbaKHnN/6op5juN6/V2IPKFGbZ91TH/FZtSkU';
var btn = document.getElementById('open-explorer');
var boot = document.getElementById('boot');
var intro = document.getElementById('static-intro');
var started = false;
var attempt = 0;
var documentFailed = false;
// Scoped compatibility fixes for the integrity-pinned Scalar 1.63.0 DOM.
// Its container CSS hides all span labels on narrow screens, including
// sr-only labels. Its language tablist also owns a non-tab menu button.
// Use existing text for names, and keep that original menu node beside
// the actual tablist. The observer reapplies this after vendor rerenders.
function improveExplorer(explorer) {
var queued = false;
function improve() {
queued = false;
// The vendor mounts its shell before the spec is fetched. A failed
// spec must not replace all useful documentation with a dead end.
// This message is emitted by the pinned bundle on document failure;
// recovery reloads the page instead of remounting vendor internals.
var heading = explorer.querySelector('h1.section-header-label');
if (!documentFailed && heading && /^Document .+ could not be loaded$/.test(heading.textContent.trim())) {
documentFailed = true;
explorer.hidden = true;
if (intro) intro.hidden = false;
document.querySelector('.skip-link').href = '#static-intro';
if (boot) {
boot.hidden = false;
boot.innerHTML = '<div class="boot-fail" role="status"><p>The API document could not load. Reload this page to try again.</p><p><a href="reference.html">Read the complete reference</a> while the interactive explorer is unavailable.</p></div>';
}
if (btn) { btn.disabled = false; btn.textContent = 'Reload this page'; }
}
if (documentFailed) return;
explorer.querySelectorAll('.client-libraries-list').forEach(function (list) {
var tabs = list.querySelector('.client-libraries-content');
var more = list.querySelector('.client-libraries__select');
if (!tabs || !more || !more.parentElement.classList.contains('contents')) return;
if (list.hasAttribute('aria-labelledby')) {
tabs.setAttribute('aria-labelledby', list.getAttribute('aria-labelledby'));
}
tabs.setAttribute('role', 'tablist');
tabs.setAttribute('aria-orientation', 'horizontal');
list.removeAttribute('role');
list.removeAttribute('aria-orientation');
list.removeAttribute('aria-labelledby');
list.classList.add('docs-client-library-tabs');
if (more.parentElement.parentElement === tabs) list.appendChild(more.parentElement);
list.querySelectorAll('[role="tab"], .client-libraries__select').forEach(function (button) {
var label = button.querySelector('.sr-only') || button.querySelector('.client-libraries-text');
if (label && label.textContent.trim()) button.setAttribute('aria-label', label.textContent.trim());
});
});
// The metadata links keep their text nodes, even when Scalar hides
// those nodes at 320px. Their accessible names must remain available.
explorer.querySelectorAll('a[href]').forEach(function (link) {
if (link.querySelector('svg') && !link.hasAttribute('aria-label') && link.textContent.trim()) {
link.setAttribute('aria-label', link.textContent.trim());
}
});
}
improve();
new MutationObserver(function () {
if (!queued) { queued = true; requestAnimationFrame(improve); }
}).observe(explorer, {childList: true, subtree: true});
}
function load() {
if (documentFailed) { location.reload(); return; }
if (started) return;
started = true;
var current = ++attempt;
var done = false;
var timer;
var obs;
if (btn) { btn.disabled = true; btn.textContent = 'Loading the explorer…'; }
if (boot) {
boot.hidden = false;
boot.innerHTML = '<div><div class="dot"></div>Loading API reference…</div>';
}
var el = document.createElement('script');
el.src = SRC;
el.integrity = SRI;
el.crossOrigin = 'anonymous';
el.onerror = fail;
document.body.appendChild(el);
function clear() {
if (done || current !== attempt) return;
done = true;
clearTimeout(timer);
if (boot) boot.hidden = true;
if (intro) intro.hidden = true;
var explorer = document.querySelector('.scalar-app');
if (explorer) {
explorer.id = 'interactive-reference';
if (!explorer.querySelector('main, [role="main"]')) explorer.setAttribute('role', 'main');
explorer.setAttribute('tabindex', '-1');
document.querySelector('.skip-link').href = '#interactive-reference';
improveExplorer(explorer);
if (!documentFailed) explorer.focus({preventScroll: true});
}
}
obs = new MutationObserver(function () {
if (document.querySelector('.scalar-app')) { clear(); obs.disconnect(); }
});
obs.observe(document.body, { childList: true, subtree: true });
timer = setTimeout(function () { if (!done) fail(); }, 15000);
function fail() {
if (done || current !== attempt) return;
done = true;
started = false;
clearTimeout(timer);
if (obs) obs.disconnect();
el.remove();
if (boot) {
boot.hidden = false;
boot.innerHTML =
'<div class="boot-fail" role="status">' +
'<p>The interactive explorer could not load. You can retry below.</p>' +
'<p><a class="lead" href="reference.html">Read the complete reference</a> ' +
'or <a href="openapi.yaml">download the OpenAPI spec</a>.</p>' +
'</div>';
}
if (btn) { btn.disabled = false; btn.textContent = 'Retry the interactive explorer'; }
}
}
if (btn) btn.addEventListener('click', load);
// A deep link into the explorer (#tag/..., ?api=...) means somebody asked for it
// explicitly — honour that and load immediately rather than making them click twice.
if (/^#(?:tag|operation|model|section)\//.test(location.hash) ||
new URLSearchParams(location.search).has('api')) load();
})();
</script>
<script>
/* First-party pageview beacon. The docs are a separate origin, so the apps' server-side
visit log never sees a docs request — yet for a developer API the docs ARE the mid-funnel,
and "did they read the docs before subscribing?" has been unanswerable because of it.
The lt_vid cookie is Domain=.livetennisapi.com, so this subdomain is same-site and the
visit stitches into the same journey as the pricing page and checkout. Allowlisted kind,
always 204, wrapped so it can never affect the page. Covered by the first-party visit log
described at https://livetennisapi.com/privacy — no new cookie is set here. */
(function(){try{navigator.sendBeacon("https://livetennisapi.com/collect",
JSON.stringify({kind:"docs_view",path:location.pathname,host:location.host}))}catch(e){}})();
</script>
<script>
/* Clarity, EU/EEA-gated — the docs were the last surface with NO on-page behaviour data.
The server-side beacon above says a page was LOADED; it cannot see scroll depth, rage
clicks, dead clicks or quick-backs, which on a reference page is most of what "did this
answer the question?" means.
ONLY Clarity. Deliberately not PostHog (autocapture puts URLs into a third-party tool and
is not geo-gated — it is what leaked 2,509 credential-bearing URLs on the apex) and not
Google Ads (nothing converts on a docs page, so a remarketing tag here would be cost
without signal).
THE GATE. This host is GitHub Pages, not Cloudflare, so there is no local /cdn-cgi/trace —
it 404s. The apex one is fetched cross-origin instead, which works because Cloudflare
answers it with `access-control-allow-origin: *`. It FAILS CLOSED, unlike the apex copy in
tennis app/routes/landing.py: an unreadable or unmatched `loc` loads nothing, because a
cross-origin fetch has more ways to return something unexpected than a same-origin one and
the wrong default here is the one that tags an EU visitor. Matches
https://livetennisapi.com/privacy. */
(function(){var E={};"AT BE BG HR CY CZ DK EE FI FR DE GR HU IE IT LV LT LU MT NL PL PT RO SK SI ES SE IS LI NO".split(" ").forEach(function(c){E[c]=1});
fetch("https://livetennisapi.com/cdn-cgi/trace").then(function(r){return r.text()}).then(function(t){
var m=t.match(/loc=(\w+)/);if(!m||E[m[1]])return;
(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y)})(window,document,"clarity","script","xotc2pctsp");
try{clarity("set","user_state","docs")}catch(e){}}).catch(function(){})})();
</script>
</body>
</html>