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 +240 -51
- package/dist/index.cjs +327 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +456 -108
- package/dist/index.d.ts +456 -108
- package/dist/index.js +327 -88
- 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,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({
|
|
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.
|
|
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.
|
|
79
|
-
expansion, rarity. Pricing (`retailPrice`, `estimatedValue`,
|
|
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({
|
|
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)` |
|
|
123
|
-
| `
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
|
134
|
-
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
|
145
|
-
|
|
146
|
-
| `
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
|
177
|
-
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
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
|
-
|
|
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
|
|
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
|
|