pitchapi 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PitchAPI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # pitchapi — TypeScript SDK
2
+
3
+ Typed TypeScript/JavaScript client for the [PitchAPI](https://pitchapi.dev)
4
+ football-data API. Match results, shots, xG, lineups, momentum, events, player
5
+ stats, head-to-head, and advanced analytics derived from the event feed.
6
+
7
+ - Zero dependencies — built on the platform `fetch` (Node 18+, Bun, Deno, edge runtimes)
8
+ - Response types generated from the API's OpenAPI description
9
+ - ESM and CommonJS builds, with type declarations for both
10
+ - Typed errors mapped from the API's stable `error.code`
11
+ - Automatic retries on `429`/`5xx` with `Retry-After` honoured
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install pitchapi
17
+ ```
18
+
19
+ ## Quick start
20
+
21
+ ```ts
22
+ import { PitchAPI } from "pitchapi";
23
+
24
+ const client = new PitchAPI({ apiKey: "pk_live_..." });
25
+
26
+ const day = await client.date.get("2026-08-27");
27
+ for (const m of day.matches) {
28
+ console.log(m.home_team.name, m.score_home, "-", m.score_away, m.away_team.name);
29
+ }
30
+
31
+ const match = await client.matches.get("m_4DP2fy");
32
+ const shots = await client.matches.shots(match.id);
33
+ const adv = await client.matches.advanced(match.id); // team rollups
34
+ const net = await client.matches.advancedNetwork(match.id); // pass networks
35
+ ```
36
+
37
+ The key can also come from the `PITCHAPI_API_KEY` environment variable, in
38
+ which case `new PitchAPI()` needs no arguments. Keep the key server-side: never
39
+ ship it in browser code.
40
+
41
+ ## Namespaces
42
+
43
+ | Namespace | Methods |
44
+ |---|---|
45
+ | `client.date` | `get(date, { status })` — `date` is a `YYYY-MM-DD` string or a `Date` (read in UTC) |
46
+ | `client.matches` | `get`, `shots`, `shot`, `events`, `lineups`, `momentum`, `stats`, `players`, `player`, `playersHalves`, `playerHalves`, `playerShots`, `h2h`, `advanced`, `advancedNetwork`, `advancedPlayers`, `advancedPlayer`, `heatmaps(id, { frame })` |
47
+ | `client.leagues` | `list`, `get`, `matches(id, { season, status })` |
48
+ | `client.teams` | `get` |
49
+ | `client.players` | `get` |
50
+
51
+ Every method takes an optional last argument `{ signal }` to abort it. Every
52
+ response type is exported by name:
53
+
54
+ ```ts
55
+ import type { Match, Shot, AdvancedTeams } from "pitchapi";
56
+ ```
57
+
58
+ ## Upcoming fixtures
59
+
60
+ Match listings return played matches by default. Pass `status` to reach
61
+ scheduled ones — `"upcoming"` for fixtures that have not kicked off, `"all"`
62
+ for both. An upcoming match carries a kickoff `time_utc` and a `status`, but
63
+ its scores are `null` until it is played.
64
+
65
+ ```ts
66
+ const { matches } = await client.date.get("2026-09-01", { status: "upcoming" });
67
+
68
+ // The season is resolved first, so asking for upcoming fixtures of a finished
69
+ // campaign is an empty list, not next season's.
70
+ await client.leagues.matches("l_0bfbkO", { season: "2025/2026", status: "all" });
71
+ ```
72
+
73
+ Lineups for a fixture may be a pre-match prediction. `confirmed` is the flag to
74
+ branch on.
75
+
76
+ ```ts
77
+ const lineups = await client.matches.lineups("m_4DP2fy");
78
+ if (lineups.home.confirmed) {
79
+ console.log(lineups.home.formation, lineups.home.starters.map((p) => p.name));
80
+ }
81
+ ```
82
+
83
+ ## Errors
84
+
85
+ ```ts
86
+ import { NotFoundError, RateLimitError } from "pitchapi";
87
+
88
+ try {
89
+ await client.matches.advanced("m_unprocessed");
90
+ } catch (e) {
91
+ if (e instanceof NotFoundError) {
92
+ // RESOURCE_NOT_FOUND, ANALYTICS_UNAVAILABLE or HALVES_UNAVAILABLE
93
+ console.log(e.code, e.requestId);
94
+ } else if (e instanceof RateLimitError) {
95
+ console.log("retry after", e.retryAfter, "s");
96
+ } else {
97
+ throw e;
98
+ }
99
+ }
100
+ ```
101
+
102
+ Every error extends `PitchAPIError` and carries `code`, `status` and
103
+ `requestId` where the API supplied them.
104
+
105
+ ## Configuration
106
+
107
+ ```ts
108
+ new PitchAPI({
109
+ apiKey: "pk_live_...",
110
+ baseUrl: "https://api.pitchapi.dev", // override for tests / proxies
111
+ timeoutMs: 30_000, // per attempt
112
+ maxRetries: 2, // 429/5xx + network errors; 0 disables
113
+ fetch: customFetch, // defaults to globalThis.fetch
114
+ });
115
+ ```
116
+
117
+ ## License
118
+
119
+ This client library is released under the MIT License.
120
+
121
+ The licence covers the SDK source only. Access to the PitchAPI service and the
122
+ football data it returns is governed separately by the PitchAPI terms of
123
+ service — an MIT-licensed client does not grant any right to the data.
package/dist/index.cjs ADDED
@@ -0,0 +1,397 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ APIConnectionError: () => APIConnectionError,
24
+ AuthError: () => AuthError,
25
+ InvalidParameterError: () => InvalidParameterError,
26
+ NotFoundError: () => NotFoundError,
27
+ PitchAPI: () => PitchAPI,
28
+ PitchAPIError: () => PitchAPIError,
29
+ PlanUpgradeRequiredError: () => PlanUpgradeRequiredError,
30
+ RateLimitError: () => RateLimitError,
31
+ ServerError: () => ServerError,
32
+ SubscriptionSuspendedError: () => SubscriptionSuspendedError,
33
+ VERSION: () => VERSION
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/errors.ts
38
+ var PitchAPIError = class extends Error {
39
+ constructor(message, options = {}) {
40
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
41
+ this.name = new.target.name;
42
+ this.code = options.code;
43
+ this.status = options.status;
44
+ this.requestId = options.requestId;
45
+ }
46
+ };
47
+ var APIConnectionError = class extends PitchAPIError {
48
+ };
49
+ var InvalidParameterError = class extends PitchAPIError {
50
+ };
51
+ var AuthError = class extends PitchAPIError {
52
+ };
53
+ var SubscriptionSuspendedError = class extends PitchAPIError {
54
+ };
55
+ var PlanUpgradeRequiredError = class extends PitchAPIError {
56
+ };
57
+ var NotFoundError = class extends PitchAPIError {
58
+ };
59
+ var RateLimitError = class extends PitchAPIError {
60
+ constructor(message, options = {}) {
61
+ super(message, options);
62
+ this.retryAfter = options.retryAfter;
63
+ }
64
+ };
65
+ var ServerError = class extends PitchAPIError {
66
+ };
67
+ var CODE_MAP = {
68
+ INVALID_PARAMETER: InvalidParameterError,
69
+ UNAUTHORIZED: AuthError,
70
+ SUBSCRIPTION_SUSPENDED: SubscriptionSuspendedError,
71
+ PLAN_UPGRADE_REQUIRED: PlanUpgradeRequiredError,
72
+ RESOURCE_NOT_FOUND: NotFoundError,
73
+ ANALYTICS_UNAVAILABLE: NotFoundError,
74
+ HALVES_UNAVAILABLE: NotFoundError,
75
+ RATE_LIMIT_EXCEEDED: RateLimitError,
76
+ INTERNAL_SERVER_ERROR: ServerError
77
+ };
78
+ function errorClassFor(code, status) {
79
+ if (code !== void 0 && Object.prototype.hasOwnProperty.call(CODE_MAP, code)) {
80
+ return CODE_MAP[code];
81
+ }
82
+ if (status === 400) return InvalidParameterError;
83
+ if (status === 401) return AuthError;
84
+ if (status === 403) return PlanUpgradeRequiredError;
85
+ if (status === 404) return NotFoundError;
86
+ if (status === 429) return RateLimitError;
87
+ if (status >= 500) return ServerError;
88
+ return PitchAPIError;
89
+ }
90
+
91
+ // src/version.ts
92
+ var VERSION = "0.1.1";
93
+
94
+ // src/transport.ts
95
+ var DEFAULT_BASE_URL = "https://api.pitchapi.dev";
96
+ var API_PREFIX = "/v1";
97
+ var RETRY_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
98
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
99
+ function isServerRuntime() {
100
+ return typeof globalThis.document === "undefined";
101
+ }
102
+ function buildHeaders(apiKey) {
103
+ const headers = {
104
+ "X-API-KEY": apiKey,
105
+ Accept: "application/json"
106
+ };
107
+ if (isServerRuntime()) headers["User-Agent"] = `pitchapi-js/${VERSION}`;
108
+ return headers;
109
+ }
110
+ function parseRetryAfter(value) {
111
+ if (!value) return void 0;
112
+ const n = Number.parseInt(value, 10);
113
+ return Number.isFinite(n) && n >= 0 ? n : void 0;
114
+ }
115
+ function backoffMs(attempt, retryAfter) {
116
+ if (retryAfter !== void 0) return retryAfter * 1e3;
117
+ const base = Math.min(2 ** attempt, 8);
118
+ return Math.random() * base * 1e3;
119
+ }
120
+ function buildQuery(params) {
121
+ if (!params) return "";
122
+ const qs = new URLSearchParams();
123
+ for (const [k, v] of Object.entries(params)) {
124
+ if (v !== void 0) qs.set(k, v);
125
+ }
126
+ const s = qs.toString();
127
+ return s ? `?${s}` : "";
128
+ }
129
+ function unwrap(status, payload, requestId, retryAfter) {
130
+ if (status >= 200 && status < 300) {
131
+ if (payload !== null && typeof payload === "object" && "data" in payload) {
132
+ return payload.data;
133
+ }
134
+ throw new PitchAPIError("malformed success response: missing 'data' envelope", { status, requestId });
135
+ }
136
+ let code;
137
+ let message = `HTTP ${status}`;
138
+ if (payload !== null && typeof payload === "object" && "error" in payload) {
139
+ const err = payload.error;
140
+ if (err !== null && typeof err === "object") {
141
+ const e = err;
142
+ if (typeof e.code === "string") code = e.code;
143
+ if (typeof e.message === "string") message = e.message;
144
+ }
145
+ }
146
+ const Cls = errorClassFor(code, status);
147
+ if (Cls === RateLimitError) {
148
+ throw new RateLimitError(message, { code, status, requestId, retryAfter });
149
+ }
150
+ if (Cls === ServerError && code === void 0) message = `server error (HTTP ${status})`;
151
+ throw new Cls(message, { code, status, requestId });
152
+ }
153
+ var Transport = class {
154
+ constructor(opts) {
155
+ this.opts = opts;
156
+ this.headers = buildHeaders(opts.apiKey);
157
+ this.sleep = opts.sleep ?? defaultSleep;
158
+ }
159
+ async get(path, query, signal) {
160
+ const url = this.opts.baseUrl + API_PREFIX + path + buildQuery(query);
161
+ let attempt = 0;
162
+ for (; ; ) {
163
+ let response;
164
+ try {
165
+ response = await this.fetchOnce(url, signal);
166
+ } catch (err) {
167
+ if (signal?.aborted) throw err;
168
+ if (attempt < this.opts.maxRetries) {
169
+ await this.sleep(backoffMs(attempt, void 0));
170
+ attempt++;
171
+ continue;
172
+ }
173
+ throw new APIConnectionError(`request failed: ${describe(err)}`, { cause: err });
174
+ }
175
+ const status = response.status;
176
+ const retryAfter = parseRetryAfter(response.headers.get("Retry-After"));
177
+ if (attempt < this.opts.maxRetries && RETRY_STATUSES.has(status)) {
178
+ await response.body?.cancel().catch(() => void 0);
179
+ await this.sleep(backoffMs(attempt, retryAfter));
180
+ attempt++;
181
+ continue;
182
+ }
183
+ const requestId = response.headers.get("X-Request-ID") ?? void 0;
184
+ let payload = null;
185
+ try {
186
+ payload = await response.json();
187
+ } catch {
188
+ }
189
+ return unwrap(status, payload, requestId, retryAfter);
190
+ }
191
+ }
192
+ async fetchOnce(url, signal) {
193
+ const timeout = AbortSignal.timeout(this.opts.timeoutMs);
194
+ const combined = signal ? anySignal([signal, timeout]) : timeout;
195
+ return this.opts.fetch(url, { method: "GET", headers: this.headers, signal: combined });
196
+ }
197
+ };
198
+ function anySignal(signals) {
199
+ const any = AbortSignal.any;
200
+ if (any) return any(signals);
201
+ const controller = new AbortController();
202
+ for (const s of signals) {
203
+ if (s.aborted) {
204
+ controller.abort(s.reason);
205
+ break;
206
+ }
207
+ s.addEventListener("abort", () => controller.abort(s.reason), { once: true });
208
+ }
209
+ return controller.signal;
210
+ }
211
+ function describe(err) {
212
+ return err instanceof Error ? err.message : String(err);
213
+ }
214
+
215
+ // src/client.ts
216
+ function env(name) {
217
+ const proc = globalThis.process;
218
+ return proc?.env?.[name] || void 0;
219
+ }
220
+ function formatDate(value) {
221
+ return typeof value === "string" ? value : value.toISOString().slice(0, 10);
222
+ }
223
+ var seg = encodeURIComponent;
224
+ var Resource = class {
225
+ constructor(t) {
226
+ this.t = t;
227
+ }
228
+ request(path, opts, query) {
229
+ return this.t.get(path, query, opts?.signal);
230
+ }
231
+ };
232
+ var DateResource = class extends Resource {
233
+ /**
234
+ * Every match on a day. `date` is a `YYYY-MM-DD` string or a `Date`, which
235
+ * is read in UTC. An upcoming fixture carries a kickoff `time_utc` and a
236
+ * `status` but no score yet.
237
+ */
238
+ get(date, opts) {
239
+ return this.request(`/date/${seg(formatDate(date))}`, opts, { status: opts?.status });
240
+ }
241
+ };
242
+ var MatchesResource = class extends Resource {
243
+ /** Match summary — league, teams, score, referee, stadium. */
244
+ get(matchId, opts) {
245
+ return this.request(`/matches/${seg(matchId)}`, opts);
246
+ }
247
+ /** Every shot, grouped by period, with pitch coordinates and xG. */
248
+ shots(matchId, opts) {
249
+ return this.request(`/matches/${seg(matchId)}/shots`, opts);
250
+ }
251
+ /** A single shot by its opaque ID. */
252
+ shot(matchId, shotId, opts) {
253
+ return this.request(`/matches/${seg(matchId)}/shots/${seg(shotId)}`, opts);
254
+ }
255
+ /** Match events (goals, cards, subs, ...). */
256
+ events(matchId, opts) {
257
+ return this.request(`/matches/${seg(matchId)}/events`, opts);
258
+ }
259
+ /** Starting lineups and substitutes. Before kickoff the XI may be predicted. */
260
+ lineups(matchId, opts) {
261
+ return this.request(`/matches/${seg(matchId)}/lineups`, opts);
262
+ }
263
+ /** Possession-momentum timeline. */
264
+ momentum(matchId, opts) {
265
+ return this.request(`/matches/${seg(matchId)}/momentum`, opts);
266
+ }
267
+ /** Team-level match statistics. */
268
+ stats(matchId, opts) {
269
+ return this.request(`/matches/${seg(matchId)}/stats`, opts);
270
+ }
271
+ /** Per-player match stat lines. */
272
+ players(matchId, opts) {
273
+ return this.request(`/matches/${seg(matchId)}/players`, opts);
274
+ }
275
+ /** One player's match stat line. */
276
+ player(matchId, playerId, opts) {
277
+ return this.request(`/matches/${seg(matchId)}/players/${seg(playerId)}`, opts);
278
+ }
279
+ /**
280
+ * Every player's stat line for the first and the second half. Throws
281
+ * `NotFoundError` (code `HALVES_UNAVAILABLE`) for a match without them.
282
+ */
283
+ playersHalves(matchId, opts) {
284
+ return this.request(`/matches/${seg(matchId)}/players/halves`, opts);
285
+ }
286
+ /** One player's stat line for each half they played. */
287
+ playerHalves(matchId, playerId, opts) {
288
+ return this.request(`/matches/${seg(matchId)}/players/${seg(playerId)}/halves`, opts);
289
+ }
290
+ /** A single player's shots in the match. */
291
+ playerShots(matchId, playerId, opts) {
292
+ return this.request(`/matches/${seg(matchId)}/players/${seg(playerId)}/shots`, opts);
293
+ }
294
+ /** Head-to-head history for the two sides. */
295
+ h2h(matchId, opts) {
296
+ return this.request(`/matches/${seg(matchId)}/h2h`, opts);
297
+ }
298
+ /**
299
+ * Team advanced rollups (PPDA, field tilt, xG timeline). Throws
300
+ * `NotFoundError` (code `ANALYTICS_UNAVAILABLE`) for a match not yet rated.
301
+ */
302
+ advanced(matchId, opts) {
303
+ return this.request(`/matches/${seg(matchId)}/advanced`, opts);
304
+ }
305
+ /** Pass networks for both sides. */
306
+ advancedNetwork(matchId, opts) {
307
+ return this.request(`/matches/${seg(matchId)}/advanced/network`, opts);
308
+ }
309
+ /** Player-level advanced rollups. */
310
+ advancedPlayers(matchId, opts) {
311
+ return this.request(`/matches/${seg(matchId)}/advanced/players`, opts);
312
+ }
313
+ /** One player's advanced rollup. */
314
+ advancedPlayer(matchId, playerId, opts) {
315
+ return this.request(`/matches/${seg(matchId)}/advanced/players/${seg(playerId)}`, opts);
316
+ }
317
+ /**
318
+ * Binned activity for both teams and every player who touched the ball.
319
+ * Cells are sparse `[cell_x, cell_y, actions]` triples on a 16x12 grid.
320
+ */
321
+ heatmaps(matchId, opts) {
322
+ return this.request(`/matches/${seg(matchId)}/heatmaps`, opts, { frame: opts?.frame });
323
+ }
324
+ };
325
+ var LeaguesResource = class extends Resource {
326
+ /** All leagues, each with its accepted seasons and coverage. */
327
+ list(opts) {
328
+ return this.request("/leagues", opts);
329
+ }
330
+ /** League detail. */
331
+ get(leagueId, opts) {
332
+ return this.request(`/leagues/${seg(leagueId)}`, opts);
333
+ }
334
+ /**
335
+ * Matches in a league. `season` is `"2025/2026"` for a fall-spring league
336
+ * and `"2026"` for a calendar-year one; omitted, the latest season with
337
+ * data. The season is resolved before `status` applies, so `upcoming` on a
338
+ * finished season is an empty list.
339
+ */
340
+ matches(leagueId, opts) {
341
+ return this.request(`/leagues/${seg(leagueId)}/matches`, opts, {
342
+ season: opts?.season,
343
+ status: opts?.status
344
+ });
345
+ }
346
+ };
347
+ var TeamsResource = class extends Resource {
348
+ /** Team profile. */
349
+ get(teamId, opts) {
350
+ return this.request(`/teams/${seg(teamId)}`, opts);
351
+ }
352
+ };
353
+ var PlayersResource = class extends Resource {
354
+ /** Player profile. */
355
+ get(playerId, opts) {
356
+ return this.request(`/players/${seg(playerId)}`, opts);
357
+ }
358
+ };
359
+ var PitchAPI = class {
360
+ constructor(options = {}) {
361
+ const apiKey = options.apiKey ?? env("PITCHAPI_API_KEY");
362
+ if (!apiKey) {
363
+ throw new Error("an API key is required: pass { apiKey } or set the PITCHAPI_API_KEY environment variable");
364
+ }
365
+ const fetchImpl = options.fetch ?? globalThis.fetch;
366
+ if (!fetchImpl) {
367
+ throw new Error("no global fetch: use Node 18+ or pass { fetch }");
368
+ }
369
+ const transport = new Transport({
370
+ apiKey,
371
+ baseUrl: (options.baseUrl ?? env("PITCHAPI_BASE_URL") ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
372
+ timeoutMs: options.timeoutMs ?? 3e4,
373
+ maxRetries: options.maxRetries ?? 2,
374
+ // Bound to globalThis: some runtimes throw on an unbound fetch.
375
+ fetch: options.fetch ?? fetchImpl.bind(globalThis)
376
+ });
377
+ this.date = new DateResource(transport);
378
+ this.matches = new MatchesResource(transport);
379
+ this.leagues = new LeaguesResource(transport);
380
+ this.teams = new TeamsResource(transport);
381
+ this.players = new PlayersResource(transport);
382
+ }
383
+ };
384
+ // Annotate the CommonJS export names for ESM import in node:
385
+ 0 && (module.exports = {
386
+ APIConnectionError,
387
+ AuthError,
388
+ InvalidParameterError,
389
+ NotFoundError,
390
+ PitchAPI,
391
+ PitchAPIError,
392
+ PlanUpgradeRequiredError,
393
+ RateLimitError,
394
+ ServerError,
395
+ SubscriptionSuspendedError,
396
+ VERSION
397
+ });