tcgpriser 0.7.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,23 +63,28 @@ 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
  ```
68
71
 
69
72
  ### Expansions
70
73
 
74
+ Cards and sealed products are never merged into one response — fetch each separately.
75
+
71
76
  ```typescript
72
77
  await tcgpriser.expansions.list();
73
- await tcgpriser.expansions.products('eng-scarlet-violet-journey-together'); // { expansion, cards, sealed } — content only
78
+ await tcgpriser.expansions.get('eng-scarlet-violet-journey-together'); // metadata only
79
+ await tcgpriser.expansions.cards('eng-scarlet-violet-journey-together'); // content only
80
+ await tcgpriser.expansions.sealedProducts('eng-scarlet-violet-journey-together'); // content only
74
81
  ```
75
82
 
76
83
  ### Pricing
77
84
 
78
- `list()`/`get()`/`expansions.products()` all return catalog content only — name, images, brand,
79
- expansion, rarity. Pricing (`retailPrice`, `estimatedValue`, `lowestShopOffer`,
80
- `referencePriceSnapshotsByProvider`) is a separate, shorter-cached call: content changes on an
85
+ `list()`/`get()`/`expansions.cards()`/`expansions.sealedProducts()` all return catalog content
86
+ only — name, images, brand, expansion, rarity. Pricing (`retailPrice`, `estimatedValue`,
87
+ `lowestShopOffer`, `referencePriceSnapshotsByProvider`) is a separate, shorter-cached call: content changes on an
81
88
  admin edit or catalog import, pricing refreshes daily, so each is cached at the TTL its own
82
89
  freshness supports.
83
90
 
@@ -86,7 +93,7 @@ await tcgpriser.cards.pricing('fezandipiti-ex'); // id or technicalName
86
93
  await tcgpriser.products.pricing('scarlet-violet-booster-pack');
87
94
 
88
95
  // Batch form, up to 200 ids at once — ids only, not technicalNames.
89
- const { data: cards } = await tcgpriser.cards.list({ search: 'pikachu' });
96
+ const { data: cards } = await tcgpriser.cards.list({ limit: 20 });
90
97
  await tcgpriser.cards.pricingBatch(cards.map((card) => card.id));
91
98
  ```
92
99
 
@@ -106,6 +113,26 @@ await tcgpriser.priceStats.estimatedValues({ expansion: 'eng-scarlet-violet-jour
106
113
  await tcgpriser.bargains.list({ type: 'sealed' }); // 'sealed' | 'card' | 'all'
107
114
  ```
108
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
+
109
136
  ### Pack rates
110
137
 
111
138
  ```typescript
@@ -115,35 +142,57 @@ await tcgpriser.packRates.get(expansionId);
115
142
 
116
143
  ## Available Methods
117
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
+
118
148
  ### `cards`
119
149
 
120
- | Method | Description |
121
- |---|---|
122
- | `list(params)` | Search or list cards |
123
- | `get(id)` | Fetch one card by id or technicalName |
124
- | `matches(id, params)` | Current shop listings matched to this card |
125
- | `prices(id, params)` 🔒 | Individual marketplace sale records |
126
- | `referencePrices(id, params)` 🔒 | Cardmarket / TCGplayer / eBay / Tradera price history |
127
- | `livePricing(id)` 🔒 | Pricing computed fresh for this request |
150
+ | Method | Description | Credits |
151
+ |---|---|---|
152
+ | `list(params)` | List cards, newest first | — |
153
+ | `search(params)` 🔒 | Free-text search on card and set names | 5 |
154
+ | `get(id)` | Fetch one card by id or technicalName | — |
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 | — |
161
+ | `prices(id, params)` 🔒 | Individual marketplace sale records | 2 |
162
+ | `referencePrices(id, params)` 🔒 | Cardmarket / TCGplayer / eBay / Tradera price history | 2 |
163
+ | `livePricing(id)` 🔒 | Pricing computed fresh for this request | 3 |
128
164
 
129
165
  ### `products`
130
166
 
131
- | Method | Description |
132
- |---|---|
133
- | `list(params)` | Search or list sealed products |
134
- | `get(id)` | Fetch one product by id or technicalName |
135
- | `matches(id, params)` | Current shop listings matched to this product |
136
- | `prices(id, params)` 🔒 | Individual marketplace sale records |
137
- | `referencePrices(id, params)` 🔒 | Cardmarket / TCGplayer / Tradera price history |
138
- | `livePricing(id)` 🔒 | Pricing computed fresh for this request |
167
+ Sealed products only. Single cards live under `cards`.
168
+
169
+ | Method | Description | Credits |
170
+ |---|---|---|
171
+ | `list(params)` | List sealed products, newest first | |
172
+ | `search(params)` 🔒 | Free-text search on the product name | 5 |
173
+ | `get(id)` | Fetch one product by id or technicalName | — |
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 | — |
180
+ | `prices(id, params)` 🔒 | Individual marketplace sale records | 2 |
181
+ | `referencePrices(id, params)` 🔒 | Cardmarket / TCGplayer / Tradera price history | 2 |
182
+ | `livePricing(id)` 🔒 | Pricing computed fresh for this request | 3 |
139
183
 
140
184
  ### `expansions`
141
185
 
142
- | Method | Description |
143
- |---|---|
144
- | `list()` | Every expansion |
145
- | `products(technicalName)` | Every card and sealed product in one expansion |
146
- | `livePricing(technicalName)` 🔒 | Fresh pricing for every item in one expansion |
186
+ Cards and sealed products are always separate calls — nothing here merges them.
187
+
188
+ | Method | Description | Credits |
189
+ |---|---|---|
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 |
147
196
 
148
197
  ### `shops`
149
198
 
@@ -162,33 +211,36 @@ await tcgpriser.packRates.get(expansionId);
162
211
 
163
212
  ### `shopMatchStats` 🔒
164
213
 
165
- | Method | Description |
166
- |---|---|
167
- | `forProduct(id, params)` | One product's price history, broken out per shop |
168
- | `forShop(shop, params)` | One shop's price history, broken out per product |
169
- | `compare(params)` | One product's price at every shop that carries it |
214
+ | Method | Description | Credits |
215
+ |---|---|---|
216
+ | `forProduct(id, params)` | One product's price history, broken out per shop | 3 |
217
+ | `forShop(shop, params)` | One shop's price history, broken out per product | 3 |
218
+ | `compare(params)` | One product's price at every shop that carries it | 3 |
170
219
 
171
220
  ### `priceStats`
172
221
 
173
- | Method | Description |
174
- |---|---|
175
- | `daily(params)` | Daily average price history |
176
- | `estimatedValues(params)` | Current estimated market value |
177
- | `topProducts(params)` | Items ranked by shop availability |
178
- | `product(id)` 🔒 | Daily history, estimate and variant summary for one product |
179
- | `productFull(id)` 🔒 | `product()` plus the item's current shop matches |
180
- | `productDaily(id, params)` 🔒 | Daily history for one product, custom window |
181
- | `productDailyLast30(id)` 🔒 | Daily history, fixed to the last 30 days |
182
- | `productEstimatedValue(id)` 🔒 | Current estimated value only |
183
- | `productByVariant(id, params)` 🔒 | Price stats per card condition/grade |
184
- | `productDailyByVariant(id, params)` 🔒 | Daily history for one condition/grade |
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
+
225
+ | Method | Description | Credits |
226
+ |---|---|---|
227
+ | `daily(params)` | Daily average price history | |
228
+ | `estimatedValues(params)` | Current estimated market value | |
229
+ | `topProducts(params)` | Items ranked by shop availability | |
230
+ | `product(id)` 🔒 | Daily history, estimate and variant summary for one product | 2 |
231
+ | `productFull(id)` 🔒 | `product()` plus the item's current shop matches | 5 |
232
+ | `productDaily(id, params)` 🔒 | Daily history for one product, custom window | 1 |
233
+ | `productDailyLast30(id)` 🔒 | Daily history, fixed to the last 30 days | 1 |
234
+ | `productEstimatedValue(id)` 🔒 | Current estimated value only | 2 |
235
+ | `productByVariant(id, params)` 🔒 | Price stats per card condition/grade | 3 |
236
+ | `productDailyByVariant(id, params)` 🔒 | Daily history for one condition/grade | 1 |
185
237
 
186
238
  ### `bargains`
187
239
 
188
- | Method | Description |
189
- |---|---|
190
- | `list(params)` | Current listings priced below their reference price |
191
- | `search(params)` 🔒 | Same, with real pagination and filters |
240
+ | Method | Description | Credits |
241
+ |---|---|---|
242
+ | `list(params)` | Current listings priced below their reference price | — |
243
+ | `search(params)` 🔒 | Same, with real pagination and filters | 5 |
192
244
 
193
245
  ### `packRates`
194
246
 
@@ -204,13 +256,24 @@ await tcgpriser.packRates.get(expansionId);
204
256
  | `submit(params)` | Submit a shop URL for scraping |
205
257
  | `assignProduct(id, params)` | Manually assign (or clear) the product a URL resolves to |
206
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
+
207
270
  ### `stats`
208
271
 
209
272
  | Method | Description |
210
273
  |---|---|
211
274
  | `platform()` | Platform-wide overview counts |
212
275
 
213
- 🔒 = premium, needs an API token. See below.
276
+ Every method also takes `signal` and `timeoutMs` see [Timeouts and cancellation](#timeouts-and-cancellation).
214
277
 
215
278
  ## Authentication
216
279
 
@@ -242,6 +305,47 @@ try {
242
305
  }
243
306
  ```
244
307
 
308
+ ## Credits
309
+
310
+ Methods marked with a credit count in the tables above draw from your account's weekly credit
311
+ allowance when called with an API token (Premium: 1500/week, Business: 6000/week, both reset Monday
312
+ 00:00 UTC). Cost is weighted by how much work the call does server-side: a cached single-item lookup
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.
316
+
317
+ Once the week's credits run out, further calls reject with `429 creditsExhausted`, surfaced the same
318
+ way as any other error:
319
+
320
+ ```typescript
321
+ try {
322
+ await tcgpriser.cards.livePricing('fezandipiti-ex');
323
+ } catch (error) {
324
+ if (error instanceof TcgPriserError && error.code === 'creditsExhausted') {
325
+ console.log('Out of credits for this week:', error.message);
326
+ }
327
+ }
328
+ ```
329
+
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
+ ```
348
+
245
349
  ## Options
246
350
 
247
351
  ```typescript
@@ -256,10 +360,83 @@ new TcgPriser({
256
360
  baseUrl: 'https://api.tcgpriser.se', // default; point at a local dev server instead
257
361
  headers: { 'User-Agent': 'my-app/1.0' },
258
362
  fetch: myCustomFetch, // defaults to global fetch (Node 18+)
363
+ timeoutMs: 60_000, // default; 0 disables the timeout entirely
259
364
  },
260
365
  });
261
366
  ```
262
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
+
263
440
  ## Types
264
441
 
265
442
  Every response type is exported from the package root:
@@ -361,7 +538,19 @@ Runs `examples/rehost-images.ts` — the "Images" section's rehosting pattern ag
361
538
 
362
539
  ## Scope
363
540
 
364
- 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.
365
554
 
366
555
  ## License
367
556