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 +175 -17
- package/dist/index.cjs +299 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +427 -62
- package/dist/index.d.ts +427 -62
- package/dist/index.js +299 -83
- 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
|
```
|
|
@@ -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({
|
|
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)` |
|
|
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)` |
|
|
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
|
-
| `
|
|
150
|
-
| `
|
|
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
|
-
|
|
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.
|
|
257
|
-
the API at 8 credits, since
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
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
|
|