tcgpriser 0.10.0 → 0.13.0

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/README.md CHANGED
@@ -14,6 +14,7 @@ A typed Node.js / browser client for the [tcgpriser.se](https://tcgpriser.se) AP
14
14
  - ⚡ **Async/await** on every method, no callbacks
15
15
  - 🛠️ **Full IntelliSense** for every method and response field
16
16
  - 📦 **Zero runtime dependencies**, built on the standard `fetch` API, ships as ESM and CJS
17
+ - ⏱️ **Timeouts and `AbortSignal`** on every call, with a sane default rather than none
17
18
  - 🔄 **Types stay in sync with the API**: `yarn generate:types` regenerates them from a live instance
18
19
 
19
20
  ## Installation
@@ -51,7 +52,8 @@ Public methods need no token. A handful of premium methods do, see [Authenticati
51
52
  ### Cards
52
53
 
53
54
  ```typescript
54
- await tcgpriser.cards.list({ search: 'pikachu', limit: 10 });
55
+ await tcgpriser.cards.list({ limit: 10 }); // newest first, no free-text search
56
+ await tcgpriser.cards.search({ search: 'pikachu' }); // premium
55
57
  await tcgpriser.cards.get('mega-evolution-ascended-heroes-fezandipiti-ex'); // id or technicalName
56
58
  await tcgpriser.cards.matches('fezandipiti-ex', { inStock: true });
57
59
  ```
@@ -61,7 +63,8 @@ await tcgpriser.cards.matches('fezandipiti-ex', { inStock: true });
61
63
  Booster boxes, ETBs, tins and the like. Single cards live under `cards`, not here.
62
64
 
63
65
  ```typescript
64
- await tcgpriser.products.list({ search: 'booster box' });
66
+ await tcgpriser.products.list({ limit: 10 }); // newest first, no free-text search
67
+ await tcgpriser.products.search({ search: 'booster box' }); // premium
65
68
  await tcgpriser.products.get('scarlet-violet-booster-pack');
66
69
  await tcgpriser.products.matches('scarlet-violet-booster-pack');
67
70
  ```
@@ -90,7 +93,7 @@ await tcgpriser.cards.pricing('fezandipiti-ex'); // id or technicalName
90
93
  await tcgpriser.products.pricing('scarlet-violet-booster-pack');
91
94
 
92
95
  // Batch form, up to 200 ids at once — ids only, not technicalNames.
93
- const { data: cards } = await tcgpriser.cards.list({ search: 'pikachu' });
96
+ const { data: cards } = await tcgpriser.cards.list({ limit: 20 });
94
97
  await tcgpriser.cards.pricingBatch(cards.map((card) => card.id));
95
98
  ```
96
99
 
@@ -110,6 +113,26 @@ await tcgpriser.priceStats.estimatedValues({ expansion: 'eng-scarlet-violet-jour
110
113
  await tcgpriser.bargains.list({ type: 'sealed' }); // 'sealed' | 'card' | 'all'
111
114
  ```
112
115
 
116
+ `priceStats` covers cards and sealed products together. To scope to one kind — so an `expansion`
117
+ filter doesn't pull in that set's single cards alongside its booster boxes — use the equivalents on
118
+ `cards` and `products`:
119
+
120
+ ```typescript
121
+ await tcgpriser.cards.dailyStats({ expansion: 'eng-scarlet-violet-journey-together' });
122
+ await tcgpriser.products.estimatedValues({ expansion: 'eng-scarlet-violet-journey-together' });
123
+ ```
124
+
125
+ ### Enumerating the catalog
126
+
127
+ `technicalNames()` returns every slug with its `updatedAt` and nothing else — no pricing joins, no
128
+ paging through full documents. It's what you want for a sitemap, or to work out which items have
129
+ changed since your last sync:
130
+
131
+ ```typescript
132
+ const { data: slugs } = await tcgpriser.cards.technicalNames();
133
+ const stale = slugs.filter((slug) => slug.updatedAt > lastSyncedAt);
134
+ ```
135
+
113
136
  ### Pack rates
114
137
 
115
138
  ```typescript
@@ -119,35 +142,57 @@ await tcgpriser.packRates.get(expansionId);
119
142
 
120
143
  ## Available Methods
121
144
 
145
+ 🔒 = premium, 🏢 = business. Both need an API token — see [Authentication](#authentication). The
146
+ Credits column applies only to calls that draw from your weekly allowance; see [Credits](#credits).
147
+
122
148
  ### `cards`
123
149
 
124
150
  | Method | Description | Credits |
125
151
  |---|---|---|
126
- | `list(params)` | Search or list cards | — |
152
+ | `list(params)` | List cards, newest first | — |
153
+ | `search(params)` 🔒 | Free-text search on card and set names | 5 |
127
154
  | `get(id)` | Fetch one card by id or technicalName | — |
128
155
  | `matches(id, params)` | Current shop listings matched to this card | — |
156
+ | `pricing(id)` | This card's current pricing snapshot | — |
157
+ | `pricingBatch(ids)` | Pricing for up to 200 cards at once, by id | — |
158
+ | `technicalNames()` | Every card's slug and `updatedAt`, for sitemaps and syncs | — |
159
+ | `dailyStats(params)` | Daily average price history, cards only | — |
160
+ | `estimatedValues(params)` | Current estimated market value, cards only | — |
129
161
  | `prices(id, params)` 🔒 | Individual marketplace sale records | 2 |
130
162
  | `referencePrices(id, params)` 🔒 | Cardmarket / TCGplayer / eBay / Tradera price history | 2 |
131
163
  | `livePricing(id)` 🔒 | Pricing computed fresh for this request | 3 |
132
164
 
133
165
  ### `products`
134
166
 
167
+ Sealed products only. Single cards live under `cards`.
168
+
135
169
  | Method | Description | Credits |
136
170
  |---|---|---|
137
- | `list(params)` | Search or list sealed products | — |
171
+ | `list(params)` | List sealed products, newest first | — |
172
+ | `search(params)` 🔒 | Free-text search on the product name | 5 |
138
173
  | `get(id)` | Fetch one product by id or technicalName | — |
139
174
  | `matches(id, params)` | Current shop listings matched to this product | — |
175
+ | `pricing(id)` | This product's current pricing snapshot | — |
176
+ | `pricingBatch(ids)` | Pricing for up to 200 products at once, by id | — |
177
+ | `technicalNames()` | Every product's slug and `updatedAt` | — |
178
+ | `dailyStats(params)` | Daily average price history, sealed only | — |
179
+ | `estimatedValues(params)` | Current estimated market value, sealed only | — |
140
180
  | `prices(id, params)` 🔒 | Individual marketplace sale records | 2 |
141
181
  | `referencePrices(id, params)` 🔒 | Cardmarket / TCGplayer / Tradera price history | 2 |
142
182
  | `livePricing(id)` 🔒 | Pricing computed fresh for this request | 3 |
143
183
 
144
184
  ### `expansions`
145
185
 
186
+ Cards and sealed products are always separate calls — nothing here merges them.
187
+
146
188
  | Method | Description | Credits |
147
189
  |---|---|---|
148
- | `list()` | Every expansion | — |
149
- | `products(technicalName)` | Every card and sealed product in one expansion | — |
150
- | `livePricing(technicalName)` 🔒 | Fresh pricing for every item in one expansion | 8 |
190
+ | `list()` | Every expansion, with counts | — |
191
+ | `get(technicalName)` | One expansion's metadata (no contents) | — |
192
+ | `cards(technicalName)` | Every card in the expansion, content only | |
193
+ | `sealedProducts(technicalName)` | Every sealed product in the expansion, content only | — |
194
+ | `cardsLivePricing(technicalName)` 🔒 | Fresh pricing for every card in the expansion | 8 |
195
+ | `productsLivePricing(technicalName)` 🔒 | Fresh pricing for every sealed product in it | 8 |
151
196
 
152
197
  ### `shops`
153
198
 
@@ -174,6 +219,9 @@ await tcgpriser.packRates.get(expansionId);
174
219
 
175
220
  ### `priceStats`
176
221
 
222
+ `daily()` and `estimatedValues()` cover cards and sealed products together. For one or the other,
223
+ use `cards.dailyStats()` / `products.dailyStats()` and their `estimatedValues()` counterparts.
224
+
177
225
  | Method | Description | Credits |
178
226
  |---|---|---|
179
227
  | `daily(params)` | Daily average price history | — |
@@ -208,15 +256,24 @@ await tcgpriser.packRates.get(expansionId);
208
256
  | `submit(params)` | Submit a shop URL for scraping |
209
257
  | `assignProduct(id, params)` | Manually assign (or clear) the product a URL resolves to |
210
258
 
259
+ ### `webhooks` 🏢
260
+
261
+ Business tier. See [Webhooks](#webhooks) below.
262
+
263
+ | Method | Description |
264
+ |---|---|
265
+ | `create(params)` | Register a webhook; returns its signing secret once |
266
+ | `list()` | Every webhook on the account |
267
+ | `delete(id)` | Revoke a webhook |
268
+ | `test(id)` | Send a sample delivery |
269
+
211
270
  ### `stats`
212
271
 
213
272
  | Method | Description |
214
273
  |---|---|
215
274
  | `platform()` | Platform-wide overview counts |
216
275
 
217
- 🔒 = premium, needs an API token. See below. The Credits column applies only to methods that draw
218
- from your weekly credit allowance when called with an API token. A session login (not available to
219
- this client) and `shopUrls` are exempt. See [Credits](#credits).
276
+ Every method also takes `signal` and `timeoutMs` see [Timeouts and cancellation](#timeouts-and-cancellation).
220
277
 
221
278
  ## Authentication
222
279
 
@@ -253,8 +310,9 @@ try {
253
310
  Methods marked with a credit count in the tables above draw from your account's weekly credit
254
311
  allowance when called with an API token (Premium: 1500/week, Business: 6000/week, both reset Monday
255
312
  00:00 UTC). Cost is weighted by how much work the call does server-side: a cached single-item lookup
256
- costs less than a whole-expansion recompute. `expansions.livePricing` is the most expensive call in
257
- the API at 8 credits, since it recomputes pricing for every card or product in the set.
313
+ costs less than a whole-expansion recompute. `expansions.cardsLivePricing()` and
314
+ `expansions.productsLivePricing()` are the most expensive calls in the API at 8 credits, since each
315
+ recomputes pricing for every item in the set.
258
316
 
259
317
  Once the week's credits run out, further calls reject with `429 creditsExhausted`, surfaced the same
260
318
  way as any other error:
@@ -269,9 +327,24 @@ try {
269
327
  }
270
328
  ```
271
329
 
272
- This client doesn't currently surface the `X-Credits-Remaining` response header. To track your
273
- remaining balance mid-week, read it off the raw HTTP response yourself outside this SDK, or poll
274
- `GET /subscription` on the main API.
330
+ To track your balance mid-week, read `creditsRemaining` off the client. It's updated from the
331
+ `X-Credits-Remaining` header the API returns on every charged response, so it costs no extra
332
+ request — but it's only as current as your last premium call, and `undefined` until you make one:
333
+
334
+ ```typescript
335
+ await tcgpriser.cards.livePricing('fezandipiti-ex');
336
+ console.log(tcgpriser.creditsRemaining); // 1487
337
+ ```
338
+
339
+ It's also on the error, which is where it matters most:
340
+
341
+ ```typescript
342
+ catch (error) {
343
+ if (error instanceof TcgPriserError && error.code === 'creditsExhausted') {
344
+ console.log(error.creditsRemaining); // 0
345
+ }
346
+ }
347
+ ```
275
348
 
276
349
  ## Options
277
350
 
@@ -287,10 +360,83 @@ new TcgPriser({
287
360
  baseUrl: 'https://api.tcgpriser.se', // default; point at a local dev server instead
288
361
  headers: { 'User-Agent': 'my-app/1.0' },
289
362
  fetch: myCustomFetch, // defaults to global fetch (Node 18+)
363
+ timeoutMs: 60_000, // default; 0 disables the timeout entirely
290
364
  },
291
365
  });
292
366
  ```
293
367
 
368
+ ## Timeouts and cancellation
369
+
370
+ Every method takes `timeoutMs` and `signal`, either as a second argument or alongside the other
371
+ params:
372
+
373
+ ```typescript
374
+ await tcgpriser.cards.get('fezandipiti-ex', { timeoutMs: 5000 });
375
+ await tcgpriser.cards.list({ limit: 10, timeoutMs: 5000 });
376
+ ```
377
+
378
+ Requests time out after 60 seconds by default. A timeout rejects with a `TcgPriserError` whose
379
+ `code` is `'timeout'` — the one code this package raises itself, so a stalled connection is always
380
+ distinguishable from a server that actually answered:
381
+
382
+ ```typescript
383
+ try {
384
+ await tcgpriser.expansions.cardsLivePricing('eng-scarlet-violet-journey-together');
385
+ } catch (error) {
386
+ if (error instanceof TcgPriserError && error.code === 'timeout') {
387
+ // A whole-expansion recompute on a large set is the one call worth raising the limit for.
388
+ await tcgpriser.expansions.cardsLivePricing('eng-scarlet-violet-journey-together', {
389
+ timeoutMs: 0, // no timeout
390
+ });
391
+ }
392
+ }
393
+ ```
394
+
395
+ Pass a `signal` to cancel from outside — a user navigating away, a request being abandoned. Whichever
396
+ fires first wins, and aborting through your own signal rejects with the standard `AbortError` rather
397
+ than a `TcgPriserError`, since that's you getting what you asked for:
398
+
399
+ ```typescript
400
+ const controller = new AbortController();
401
+ setTimeout(() => controller.abort(), 1000);
402
+ await tcgpriser.cards.list({ limit: 10, signal: controller.signal });
403
+ ```
404
+
405
+ ## Rate limits
406
+
407
+ Anonymous traffic is capped per IP, and premium reads per token (Premium 30/min, Business 120/min).
408
+ Going over rejects with `429 rateLimited`, carrying the seconds to wait:
409
+
410
+ ```typescript
411
+ if (error instanceof TcgPriserError && error.code === 'rateLimited') {
412
+ await sleep((error.retryAfter ?? 60) * 1000);
413
+ }
414
+ ```
415
+
416
+ ## Webhooks
417
+
418
+ Business tier only — a Premium token gets `403 businessRequired`. Instead of polling, the API POSTs
419
+ to a URL you register when a catalog event fires.
420
+
421
+ ```typescript
422
+ const tcgpriser = new TcgPriser(myBusinessApiToken);
423
+
424
+ const webhook = await tcgpriser.webhooks.create({
425
+ url: 'https://example.com/hooks/tcgpriser', // must be https
426
+ events: ['price.updated', 'bargain.found'],
427
+ });
428
+
429
+ // The only time you will ever see this. Store it now — deliveries are signed with it, and no
430
+ // endpoint reads it back. Lost it? Delete the webhook and register a new one.
431
+ await saveSecret(webhook.secret);
432
+
433
+ await tcgpriser.webhooks.test(webhook.id); // sample delivery, so you can verify your endpoint
434
+ await tcgpriser.webhooks.list(); // never includes secrets
435
+ await tcgpriser.webhooks.delete(webhook.id);
436
+ ```
437
+
438
+ Available events: `price.updated`, `bargain.found`, `product.created`, `card.created`.
439
+
294
440
  ## Types
295
441
 
296
442
  Every response type is exported from the package root:
@@ -392,7 +538,19 @@ Runs `examples/rehost-images.ts` — the "Images" section's rehosting pattern ag
392
538
 
393
539
  ## Scope
394
540
 
395
- Covers the API's full documented surface: public catalog, price and bargain reads, plus the premium endpoints above. Not covered: the admin/scraper/auth surface, which isn't part of any published contract.
541
+ Covers everything on the API's two published documentation pages: the public catalog, price and
542
+ bargain reads at [/docs](https://api.tcgpriser.se/docs), and the premium and business endpoints at
543
+ [/premium-docs](https://api.tcgpriser.se/premium-docs).
544
+
545
+ Not covered, deliberately:
546
+
547
+ - **The admin, scraper and ingest surface.** Ours, not a customer's — no API token reaches it, and
548
+ it isn't part of any published contract.
549
+ - **Account management** (login, subscriptions, API-token minting, referrals). These authenticate
550
+ with the website's session JWT, which comes from an OAuth flow this client can't drive. Generate
551
+ your API token from your account page instead.
552
+ - **`/bulk-export`.** Feeds our own static build, answers a non-standard shape, and is not
553
+ documented for third parties. Page the documented endpoints instead.
396
554
 
397
555
  ## License
398
556