tcgpriser 0.10.0 β 1.0.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 +214 -19
- package/dist/index.cjs +335 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +576 -79
- package/dist/index.d.ts +576 -79
- package/dist/index.js +335 -85
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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({
|
|
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({
|
|
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
|
```
|
|
@@ -77,6 +80,31 @@ await tcgpriser.expansions.cards('eng-scarlet-violet-journey-together'); // cont
|
|
|
77
80
|
await tcgpriser.expansions.sealedProducts('eng-scarlet-violet-journey-together'); // content only
|
|
78
81
|
```
|
|
79
82
|
|
|
83
|
+
### Brands
|
|
84
|
+
|
|
85
|
+
The franchises and makers the catalogue carries β today just PokΓ©mon, but `cards`/`products`/
|
|
86
|
+
`expansions` all accept a `brand` filter (an `id` or `technicalName`) in preparation for more.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
await tcgpriser.brands.list();
|
|
90
|
+
await tcgpriser.brands.get('pokemon');
|
|
91
|
+
|
|
92
|
+
// Scope any list to one brand. An unrecognized value is a 400, not a silently empty page.
|
|
93
|
+
await tcgpriser.cards.list({ brand: 'pokemon' });
|
|
94
|
+
await tcgpriser.expansions.list({ brand: 'pokemon' });
|
|
95
|
+
|
|
96
|
+
// Disambiguate a technicalName two brands might both use, on a single-item lookup.
|
|
97
|
+
await tcgpriser.products.get('booster-box', { brand: 'pokemon' });
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`productLine` narrows further, to what kind of catalogue item something is β `'tcg'` for every
|
|
101
|
+
card and sealed product today, plus `'accessory'` / `'collectible'` / `'boardGame'` /
|
|
102
|
+
`'videoGame'` / `'other'` for non-TCG items as the catalogue grows to carry them:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
await tcgpriser.cards.list({ productLine: 'tcg' });
|
|
106
|
+
```
|
|
107
|
+
|
|
80
108
|
### Pricing
|
|
81
109
|
|
|
82
110
|
`list()`/`get()`/`expansions.cards()`/`expansions.sealedProducts()` all return catalog content
|
|
@@ -90,7 +118,7 @@ await tcgpriser.cards.pricing('fezandipiti-ex'); // id or technicalName
|
|
|
90
118
|
await tcgpriser.products.pricing('scarlet-violet-booster-pack');
|
|
91
119
|
|
|
92
120
|
// Batch form, up to 200 ids at once β ids only, not technicalNames.
|
|
93
|
-
const { data: cards } = await tcgpriser.cards.list({
|
|
121
|
+
const { data: cards } = await tcgpriser.cards.list({ limit: 20 });
|
|
94
122
|
await tcgpriser.cards.pricingBatch(cards.map((card) => card.id));
|
|
95
123
|
```
|
|
96
124
|
|
|
@@ -110,6 +138,26 @@ await tcgpriser.priceStats.estimatedValues({ expansion: 'eng-scarlet-violet-jour
|
|
|
110
138
|
await tcgpriser.bargains.list({ type: 'sealed' }); // 'sealed' | 'card' | 'all'
|
|
111
139
|
```
|
|
112
140
|
|
|
141
|
+
`priceStats` covers cards and sealed products together. To scope to one kind β so an `expansion`
|
|
142
|
+
filter doesn't pull in that set's single cards alongside its booster boxes β use the equivalents on
|
|
143
|
+
`cards` and `products`:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
await tcgpriser.cards.dailyStats({ expansion: 'eng-scarlet-violet-journey-together' });
|
|
147
|
+
await tcgpriser.products.estimatedValues({ expansion: 'eng-scarlet-violet-journey-together' });
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Enumerating the catalog
|
|
151
|
+
|
|
152
|
+
`technicalNames()` returns every slug with its `updatedAt` and nothing else β no pricing joins, no
|
|
153
|
+
paging through full documents. It's what you want for a sitemap, or to work out which items have
|
|
154
|
+
changed since your last sync:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
const { data: slugs } = await tcgpriser.cards.technicalNames();
|
|
158
|
+
const stale = slugs.filter((slug) => slug.updatedAt > lastSyncedAt);
|
|
159
|
+
```
|
|
160
|
+
|
|
113
161
|
### Pack rates
|
|
114
162
|
|
|
115
163
|
```typescript
|
|
@@ -119,35 +167,69 @@ await tcgpriser.packRates.get(expansionId);
|
|
|
119
167
|
|
|
120
168
|
## Available Methods
|
|
121
169
|
|
|
170
|
+
π = premium, π’ = business. Both need an API token β see [Authentication](#authentication). The
|
|
171
|
+
Credits column applies only to calls that draw from your weekly allowance; see [Credits](#credits).
|
|
172
|
+
|
|
122
173
|
### `cards`
|
|
123
174
|
|
|
175
|
+
`list()`/`dailyStats()`/`estimatedValues()` all take `brand`/`productLine` filters β see
|
|
176
|
+
[Brands](#brands). `get()` takes `brand` too, to disambiguate a technicalName two brands share.
|
|
177
|
+
|
|
124
178
|
| Method | Description | Credits |
|
|
125
179
|
|---|---|---|
|
|
126
|
-
| `list(params)` |
|
|
127
|
-
| `
|
|
180
|
+
| `list(params)` | List cards, newest first | β |
|
|
181
|
+
| `search(params)` π | Free-text search on card and set names | 5 |
|
|
182
|
+
| `get(id, params)` | Fetch one card by id or technicalName | β |
|
|
128
183
|
| `matches(id, params)` | Current shop listings matched to this card | β |
|
|
184
|
+
| `pricing(id)` | This card's current pricing snapshot | β |
|
|
185
|
+
| `pricingBatch(ids)` | Pricing for up to 200 cards at once, by id | β |
|
|
186
|
+
| `technicalNames()` | Every card's slug and `updatedAt`, for sitemaps and syncs | β |
|
|
187
|
+
| `dailyStats(params)` | Daily average price history, cards only | β |
|
|
188
|
+
| `estimatedValues(params)` | Current estimated market value, cards only | β |
|
|
129
189
|
| `prices(id, params)` π | Individual marketplace sale records | 2 |
|
|
130
190
|
| `referencePrices(id, params)` π | Cardmarket / TCGplayer / eBay / Tradera price history | 2 |
|
|
131
191
|
| `livePricing(id)` π | Pricing computed fresh for this request | 3 |
|
|
132
192
|
|
|
133
193
|
### `products`
|
|
134
194
|
|
|
195
|
+
Sealed products only. Single cards live under `cards`. `list()`/`dailyStats()`/`estimatedValues()`
|
|
196
|
+
take `brand`/`productLine` filters, and `get()` takes `brand` β see [Brands](#brands).
|
|
197
|
+
|
|
135
198
|
| Method | Description | Credits |
|
|
136
199
|
|---|---|---|
|
|
137
|
-
| `list(params)` |
|
|
138
|
-
| `
|
|
200
|
+
| `list(params)` | List sealed products, newest first | β |
|
|
201
|
+
| `search(params)` π | Free-text search on the product name | 5 |
|
|
202
|
+
| `get(id, params)` | Fetch one product by id or technicalName | β |
|
|
139
203
|
| `matches(id, params)` | Current shop listings matched to this product | β |
|
|
204
|
+
| `pricing(id)` | This product's current pricing snapshot | β |
|
|
205
|
+
| `pricingBatch(ids)` | Pricing for up to 200 products at once, by id | β |
|
|
206
|
+
| `technicalNames()` | Every product's slug and `updatedAt` | β |
|
|
207
|
+
| `dailyStats(params)` | Daily average price history, sealed only | β |
|
|
208
|
+
| `estimatedValues(params)` | Current estimated market value, sealed only | β |
|
|
140
209
|
| `prices(id, params)` π | Individual marketplace sale records | 2 |
|
|
141
210
|
| `referencePrices(id, params)` π | Cardmarket / TCGplayer / Tradera price history | 2 |
|
|
142
211
|
| `livePricing(id)` π | Pricing computed fresh for this request | 3 |
|
|
143
212
|
|
|
144
213
|
### `expansions`
|
|
145
214
|
|
|
215
|
+
Cards and sealed products are always separate calls β nothing here merges them. `list()` takes a
|
|
216
|
+
`brand` filter β see [Brands](#brands).
|
|
217
|
+
|
|
146
218
|
| Method | Description | Credits |
|
|
147
219
|
|---|---|---|
|
|
148
|
-
| `list()` | Every expansion | β |
|
|
149
|
-
| `
|
|
150
|
-
| `
|
|
220
|
+
| `list(params)` | Every expansion, with counts | β |
|
|
221
|
+
| `get(technicalName)` | One expansion's metadata (no contents) | β |
|
|
222
|
+
| `cards(technicalName)` | Every card in the expansion, content only | β |
|
|
223
|
+
| `sealedProducts(technicalName)` | Every sealed product in the expansion, content only | β |
|
|
224
|
+
| `cardsLivePricing(technicalName)` π | Fresh pricing for every card in the expansion | 8 |
|
|
225
|
+
| `productsLivePricing(technicalName)` π | Fresh pricing for every sealed product in it | 8 |
|
|
226
|
+
|
|
227
|
+
### `brands`
|
|
228
|
+
|
|
229
|
+
| Method | Description |
|
|
230
|
+
|---|---|
|
|
231
|
+
| `list()` | Every brand the catalogue carries |
|
|
232
|
+
| `get(id)` | Fetch one brand by id or technicalName |
|
|
151
233
|
|
|
152
234
|
### `shops`
|
|
153
235
|
|
|
@@ -174,6 +256,9 @@ await tcgpriser.packRates.get(expansionId);
|
|
|
174
256
|
|
|
175
257
|
### `priceStats`
|
|
176
258
|
|
|
259
|
+
`daily()` and `estimatedValues()` cover cards and sealed products together. For one or the other,
|
|
260
|
+
use `cards.dailyStats()` / `products.dailyStats()` and their `estimatedValues()` counterparts.
|
|
261
|
+
|
|
177
262
|
| Method | Description | Credits |
|
|
178
263
|
|---|---|---|
|
|
179
264
|
| `daily(params)` | Daily average price history | β |
|
|
@@ -208,15 +293,24 @@ await tcgpriser.packRates.get(expansionId);
|
|
|
208
293
|
| `submit(params)` | Submit a shop URL for scraping |
|
|
209
294
|
| `assignProduct(id, params)` | Manually assign (or clear) the product a URL resolves to |
|
|
210
295
|
|
|
296
|
+
### `webhooks` π’
|
|
297
|
+
|
|
298
|
+
Business tier. See [Webhooks](#webhooks) below.
|
|
299
|
+
|
|
300
|
+
| Method | Description |
|
|
301
|
+
|---|---|
|
|
302
|
+
| `create(params)` | Register a webhook; returns its signing secret once |
|
|
303
|
+
| `list()` | Every webhook on the account |
|
|
304
|
+
| `delete(id)` | Revoke a webhook |
|
|
305
|
+
| `test(id)` | Send a sample delivery |
|
|
306
|
+
|
|
211
307
|
### `stats`
|
|
212
308
|
|
|
213
309
|
| Method | Description |
|
|
214
310
|
|---|---|
|
|
215
311
|
| `platform()` | Platform-wide overview counts |
|
|
216
312
|
|
|
217
|
-
|
|
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).
|
|
313
|
+
Every method also takes `signal` and `timeoutMs` β see [Timeouts and cancellation](#timeouts-and-cancellation).
|
|
220
314
|
|
|
221
315
|
## Authentication
|
|
222
316
|
|
|
@@ -253,8 +347,9 @@ try {
|
|
|
253
347
|
Methods marked with a credit count in the tables above draw from your account's weekly credit
|
|
254
348
|
allowance when called with an API token (Premium: 1500/week, Business: 6000/week, both reset Monday
|
|
255
349
|
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.
|
|
257
|
-
the API at 8 credits, since
|
|
350
|
+
costs less than a whole-expansion recompute. `expansions.cardsLivePricing()` and
|
|
351
|
+
`expansions.productsLivePricing()` are the most expensive calls in the API at 8 credits, since each
|
|
352
|
+
recomputes pricing for every item in the set.
|
|
258
353
|
|
|
259
354
|
Once the week's credits run out, further calls reject with `429 creditsExhausted`, surfaced the same
|
|
260
355
|
way as any other error:
|
|
@@ -269,9 +364,24 @@ try {
|
|
|
269
364
|
}
|
|
270
365
|
```
|
|
271
366
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
367
|
+
To track your balance mid-week, read `creditsRemaining` off the client. It's updated from the
|
|
368
|
+
`X-Credits-Remaining` header the API returns on every charged response, so it costs no extra
|
|
369
|
+
request β but it's only as current as your last premium call, and `undefined` until you make one:
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
await tcgpriser.cards.livePricing('fezandipiti-ex');
|
|
373
|
+
console.log(tcgpriser.creditsRemaining); // 1487
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
It's also on the error, which is where it matters most:
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
catch (error) {
|
|
380
|
+
if (error instanceof TcgPriserError && error.code === 'creditsExhausted') {
|
|
381
|
+
console.log(error.creditsRemaining); // 0
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
```
|
|
275
385
|
|
|
276
386
|
## Options
|
|
277
387
|
|
|
@@ -287,10 +397,83 @@ new TcgPriser({
|
|
|
287
397
|
baseUrl: 'https://api.tcgpriser.se', // default; point at a local dev server instead
|
|
288
398
|
headers: { 'User-Agent': 'my-app/1.0' },
|
|
289
399
|
fetch: myCustomFetch, // defaults to global fetch (Node 18+)
|
|
400
|
+
timeoutMs: 60_000, // default; 0 disables the timeout entirely
|
|
290
401
|
},
|
|
291
402
|
});
|
|
292
403
|
```
|
|
293
404
|
|
|
405
|
+
## Timeouts and cancellation
|
|
406
|
+
|
|
407
|
+
Every method takes `timeoutMs` and `signal`, either as a second argument or alongside the other
|
|
408
|
+
params:
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
await tcgpriser.cards.get('fezandipiti-ex', { timeoutMs: 5000 });
|
|
412
|
+
await tcgpriser.cards.list({ limit: 10, timeoutMs: 5000 });
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Requests time out after 60 seconds by default. A timeout rejects with a `TcgPriserError` whose
|
|
416
|
+
`code` is `'timeout'` β the one code this package raises itself, so a stalled connection is always
|
|
417
|
+
distinguishable from a server that actually answered:
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
try {
|
|
421
|
+
await tcgpriser.expansions.cardsLivePricing('eng-scarlet-violet-journey-together');
|
|
422
|
+
} catch (error) {
|
|
423
|
+
if (error instanceof TcgPriserError && error.code === 'timeout') {
|
|
424
|
+
// A whole-expansion recompute on a large set is the one call worth raising the limit for.
|
|
425
|
+
await tcgpriser.expansions.cardsLivePricing('eng-scarlet-violet-journey-together', {
|
|
426
|
+
timeoutMs: 0, // no timeout
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Pass a `signal` to cancel from outside β a user navigating away, a request being abandoned. Whichever
|
|
433
|
+
fires first wins, and aborting through your own signal rejects with the standard `AbortError` rather
|
|
434
|
+
than a `TcgPriserError`, since that's you getting what you asked for:
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
const controller = new AbortController();
|
|
438
|
+
setTimeout(() => controller.abort(), 1000);
|
|
439
|
+
await tcgpriser.cards.list({ limit: 10, signal: controller.signal });
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## Rate limits
|
|
443
|
+
|
|
444
|
+
Anonymous traffic is capped per IP, and premium reads per token (Premium 30/min, Business 120/min).
|
|
445
|
+
Going over rejects with `429 rateLimited`, carrying the seconds to wait:
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
if (error instanceof TcgPriserError && error.code === 'rateLimited') {
|
|
449
|
+
await sleep((error.retryAfter ?? 60) * 1000);
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
## Webhooks
|
|
454
|
+
|
|
455
|
+
Business tier only β a Premium token gets `403 businessRequired`. Instead of polling, the API POSTs
|
|
456
|
+
to a URL you register when a catalog event fires.
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
const tcgpriser = new TcgPriser(myBusinessApiToken);
|
|
460
|
+
|
|
461
|
+
const webhook = await tcgpriser.webhooks.create({
|
|
462
|
+
url: 'https://example.com/hooks/tcgpriser', // must be https
|
|
463
|
+
events: ['price.updated', 'bargain.found'],
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// The only time you will ever see this. Store it now β deliveries are signed with it, and no
|
|
467
|
+
// endpoint reads it back. Lost it? Delete the webhook and register a new one.
|
|
468
|
+
await saveSecret(webhook.secret);
|
|
469
|
+
|
|
470
|
+
await tcgpriser.webhooks.test(webhook.id); // sample delivery, so you can verify your endpoint
|
|
471
|
+
await tcgpriser.webhooks.list(); // never includes secrets
|
|
472
|
+
await tcgpriser.webhooks.delete(webhook.id);
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Available events: `price.updated`, `bargain.found`, `product.created`, `card.created`.
|
|
476
|
+
|
|
294
477
|
## Types
|
|
295
478
|
|
|
296
479
|
Every response type is exported from the package root:
|
|
@@ -392,7 +575,19 @@ Runs `examples/rehost-images.ts` β the "Images" section's rehosting pattern ag
|
|
|
392
575
|
|
|
393
576
|
## Scope
|
|
394
577
|
|
|
395
|
-
Covers the API's
|
|
578
|
+
Covers everything on the API's two published documentation pages: the public catalog, price and
|
|
579
|
+
bargain reads at [/docs](https://api.tcgpriser.se/docs), and the premium and business endpoints at
|
|
580
|
+
[/premium-docs](https://api.tcgpriser.se/premium-docs).
|
|
581
|
+
|
|
582
|
+
Not covered, deliberately:
|
|
583
|
+
|
|
584
|
+
- **The admin, scraper and ingest surface.** Ours, not a customer's β no API token reaches it, and
|
|
585
|
+
it isn't part of any published contract.
|
|
586
|
+
- **Account management** (login, subscriptions, API-token minting, referrals). These authenticate
|
|
587
|
+
with the website's session JWT, which comes from an OAuth flow this client can't drive. Generate
|
|
588
|
+
your API token from your account page instead.
|
|
589
|
+
- **`/bulk-export`.** Feeds our own static build, answers a non-standard shape, and is not
|
|
590
|
+
documented for third parties. Page the documented endpoints instead.
|
|
396
591
|
|
|
397
592
|
## License
|
|
398
593
|
|