@lonca/trendyol 0.5.0 → 0.7.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
@@ -4,24 +4,38 @@
4
4
 
5
5
  Type-safe TypeScript SDK for the [Trendyol Marketplace API](https://developers.trendyol.com).
6
6
 
7
- > **`0.3.0` — bottom-up Phase 2 complete.** The full product surface is now wire-verified: brands · categories · suppliers · products (read + write + lifecycle) · inventory · orders.
7
+ > **`0.6.0` — Trendyol surface complete.** 14 resources, ~70 typed methods, plus a `parseWebhookEvent` helper for inbound event handling. Every endpoint a non-AutoFT non-V1 seller can hit is covered.
8
8
 
9
9
  ## Coverage
10
10
 
11
- Each entry is a method on the client. Endpoints behind `★` are discovery-first wire-verified against live Trendyol STAGE — the SDK normalizes any spec/wire mismatch.
12
-
13
- | Resource | Methods |
14
- | ---------------- | -------------------------------------------------------------------------------------------------------------------------- |
15
- | `brands` | `list()`, `search(name)` ★ |
16
- | `categories` | `list()`, `getAttributes(id)`, `getAttributeValues(catId, attrId)` ★, `getByBarcodes(barcodes)` (AutoFT) |
17
- | `suppliers` | `getAddresses({forceRefresh?})` (1-hour cache; rate-limited 1 req/hour on Trendyol) |
18
- | `products` | `list({...})` ★, `listUnapproved({...})` ★, `getBase(barcode)`, `getBuyboxInfo(barcodes)`, `getBatchStatus(id)` |
19
- | `products` write | `create(items)`, `updateContent(items)`, `updateVariants(items)`, `updateUnapproved(items)` ★, `updateDeliveryInfo(items)` |
20
- | `products` life | `delete(barcodes)`, `archive(barcodes)`, `unarchive(barcodes)`, `unlock(barcodes)` |
21
- | `inventory` | `update(items)` (stock + price, async batch) ★ |
22
- | `orders` | `list({...})` (shipment packages) ★ |
23
-
24
- Trendyol's product API has 12 V2 read/write/lifecycle endpoints — all 12 are covered. Categories V2 (tree + attributes + values + barcode→category lookup) is complete. V1 endpoints are intentionally skipped (Trendyol sunsets them August 2026). Order sub-endpoints (`updatePackageStatus`, `splitShipmentPackage`, returns, claims, webhooks) land in subsequent phases.
11
+ Each entry is a method on the client. Endpoints marked `★` are discovery-first wire-verified against live Trendyol STAGE — the SDK normalizes any spec/wire mismatch.
12
+
13
+ | Resource | Methods |
14
+ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
15
+ | `brands` | `list()`, `search(name)` ★ |
16
+ | `categories` | `list()`, `getAttributes(id)`, `getAttributeValues(catId, attrId)` ★, `getByBarcodes(barcodes)` (AutoFT) |
17
+ | `suppliers` | `getAddresses({forceRefresh?})` (1-hour cache; rate-limited 1 req/hour on Trendyol) |
18
+ | `products` read | `list({...})` ★, `listUnapproved({...})` ★, `getBase(barcode)` ★, `getBuyboxInfo(barcodes)` ★, `getBatchStatus(id)` ★ |
19
+ | `products` write | `create(items)`, `updateContent(items)`, `updateVariants(items)`, `updateUnapproved(items)` ★, `updateDeliveryInfo(items)` |
20
+ | `products` life | `delete(barcodes)`, `archive(barcodes)`, `unarchive(barcodes)`, `unlock(barcodes)` |
21
+ | `inventory` | `update(items)` ★ (stock + price, async batch) |
22
+ | `orders` read | `list({...})` ★, `listStream({...})` ★ (opaque cursor for >10K), `getCargoInvoiceItems(serial, {...})` |
23
+ | `orders` write | `updatePackageStatus(id, {...})`, `cancelPackageItem(id, {...})`, `extendDeliveryDate(id, 1\|2\|3)`, `processAlternativeDelivery(id, {...})` |
24
+ | `orders` split | `splitPackage`, `splitPackageByQuantity`, `multiSplitPackage`, `splitMultiPackagesByQuantity` (4 variants) |
25
+ | `orders` cargo | `changeCargoProvider(id, code)`, `manualDeliverByPackageId(id)`, `manualDeliverByTrackingNumber(trk)`, `markDeliveredByService(id)` |
26
+ | `orders` ops | `updateBoxInfo(id, {...})`, `updateLaborCosts(id, items)`, `updateWarehouse(id, warehouseId)` |
27
+ | `orders` returns | `manualReturnByPackageId(id)`, `manualReturnByTrackingNumber(trk)`, `getCompensationTickets({...})` (TEX) |
28
+ | `claims` | `create({...})`, `createIssue(id, {...})` (multipart), `approveLineItems(id, {...})`, `list({...})`, `getIssueReasons()` ★, `getItemAudits(itemId)` ★ |
29
+ | `webhooks` | `create({...})`, `list()`, `update(id, {...})`, `delete(id)`, `activate(id)`, `deactivate(id)` |
30
+ | `questions` | `get(id)`, `list({...})` ★, `answer(id, text)` |
31
+ | `invoices` | `uploadFile({shipmentPackageId, file, ...})` (multipart), `sendLink({...})`, `deleteLink({...})` |
32
+ | `finance` | `getSettlements({...})`, `getOtherFinancials({...})` — both return typed `FinancialTransaction[]` ★ |
33
+ | `labels` | `createCommon(trackingNumber, {format: 'ZPL', ...})`, `getCommon(trackingNumber)` ★ |
34
+ | `testOrders` | `create({...})`, `updateStatus(id, status)`, `setClaimsWaitingInAction()` — **STAGE-only utility** |
35
+ | `locations` | `getCountries()` ★, `getTurkeyCities()` ★, `getTurkeyDistricts(cityCode)`, `getTurkeyNeighborhoods(cityCode, districtCode)`, `getAzerbaijanCities()`, `getAzerbaijanDistricts(...)`, `getCitiesByCountry/getDistrictsByCity(...)` |
36
+ | **top-level** | `parseWebhookEvent(rawBody)`, `normalizeShipmentPackage(rawNode)` — for inbound webhook handlers |
37
+
38
+ **Intentionally excluded:** V1 endpoints (Trendyol sunsets them August 2026), AutoFT (Export Center) program-specific endpoints other than `getByBarcodes`, `processAlternativeDeliveryDigital` (digital products only).
25
39
 
26
40
  ## Install
27
41
 
@@ -43,10 +57,9 @@ const client = createTrendyolClient({
43
57
  apiKey: process.env.TRENDYOL_API_KEY!,
44
58
  apiSecret: process.env.TRENDYOL_API_SECRET!,
45
59
  env: 'stage', // or 'prod'
46
- integratorName: 'MyCompany', // optional; defaults to 'SelfIntegration'
60
+ integratorName: 'MyCompany', // required; use 'SelfIntegration' if the seller owns the integration code
47
61
  });
48
62
 
49
- // Iterate every product page-by-page.
50
63
  for await (const product of paginate((p) => client.products.list(p))) {
51
64
  for (const variant of product.variants) {
52
65
  console.log(variant.barcode, product.title, variant.stock ?? '?');
@@ -54,32 +67,26 @@ for await (const product of paginate((p) => client.products.list(p))) {
54
67
  }
55
68
  ```
56
69
 
57
- ## End-to-end: create a product
58
-
59
- The chain `brand → category → category attributes (+ values) → addresses → create → poll → verify` is the canonical "create a real listing" flow.
70
+ ## End-to-end flows
60
71
 
61
- ```ts
62
- import { createTrendyolClient } from '@lonca/trendyol';
72
+ ### Create a product
63
73
 
64
- const client = createTrendyolClient({ ... });
74
+ The chain `brand → category → attributes (+ values) → addresses → create → poll → verify`:
65
75
 
66
- // 1. Resolve brand + category IDs.
76
+ ```ts
67
77
  const [brand] = await client.brands.search('TRENDYOLMİLLA');
68
78
  const tree = await client.categories.list();
69
79
  const category = findLeaf(tree, /Elbise/); // your own walker
70
80
 
71
- // 2. Fetch required attributes + a value (Renk = Kırmızı).
72
81
  const attrs = await client.categories.getAttributes(category.id);
73
82
  const renk = attrs.find((a) => a.name === 'Renk')!;
74
83
  const renkValues = await client.categories.getAttributeValues(category.id, renk.id);
75
84
  const kirmizi = renkValues.items.find((v) => v.name === 'Kırmızı')!;
76
85
 
77
- // 3. Resolve shipment / returning warehouse IDs.
78
86
  const addresses = await client.suppliers.getAddresses();
79
87
  const shipment = addresses.find((a) => a.isShipmentAddress)!;
80
88
  const returning = addresses.find((a) => a.isReturningAddress)!;
81
89
 
82
- // 4. Submit the create (async batch).
83
90
  const { batchRequestId } = await client.products.create([
84
91
  {
85
92
  barcode: 'MY-SKU-001',
@@ -101,20 +108,111 @@ const { batchRequestId } = await client.products.create([
101
108
  },
102
109
  ]);
103
110
 
104
- // 5. Poll until Trendyol finishes content review.
111
+ // Poll the batch → detect approval.
105
112
  let result;
106
113
  do {
107
114
  await new Promise((r) => setTimeout(r, 2000));
108
115
  result = await client.products.getBatchStatus(batchRequestId);
109
116
  } while (result.items[0]?.status === 'PROCESSING');
110
117
 
111
- // 6. Detect approval (or surface a rejection reason).
112
118
  if (result.items[0]?.status === 'SUCCESS') {
113
119
  const base = await client.products.getBase('MY-SKU-001');
114
120
  console.log('Approved:', base.approved, 'contentId:', base.contentId);
115
121
  }
116
122
  ```
117
123
 
124
+ ### Handle inbound webhooks (Express)
125
+
126
+ Trendyol POSTs the same body shape as `getShipmentPackages` to your endpoint on status events. `parseWebhookEvent` returns typed `ShipmentPackage[]`:
127
+
128
+ ```ts
129
+ import express from 'express';
130
+ import { parseWebhookEvent } from '@lonca/trendyol';
131
+
132
+ const app = express();
133
+
134
+ app.post('/trendyol/webhook', express.json(), (req, res) => {
135
+ // Authenticate Trendyol against your endpoint here (Basic or x-api-key,
136
+ // matching the auth method you configured on the subscription).
137
+
138
+ const event = parseWebhookEvent(req.body);
139
+ for (const pkg of event.packages) {
140
+ // pkg: typed ShipmentPackage — same shape as orders.list()
141
+ await myQueue.enqueue({
142
+ packageId: pkg.id,
143
+ orderNumber: pkg.orderNumber,
144
+ status: pkg.status,
145
+ createdBy: pkg.raw.createdBy, // 'order-creation' | 'cancel' | 'split' | 'transfer'
146
+ });
147
+ }
148
+ res.sendStatus(200);
149
+ });
150
+
151
+ // Register the subscription once.
152
+ await client.webhooks.create({
153
+ url: 'https://my-app.example.com/trendyol/webhook',
154
+ authenticationType: 'API_KEY',
155
+ apiKey: process.env.TRENDYOL_WEBHOOK_API_KEY!,
156
+ subscribedStatuses: ['CREATED', 'SHIPPED', 'DELIVERED'],
157
+ });
158
+ ```
159
+
160
+ > **Important:** Trendyol authenticates against _your_ endpoint with the auth method you choose. There's no HMAC signature — pick `API_KEY` over `BASIC_AUTHENTICATION` so you can rotate the secret without redeploying. Trendyol retries failed deliveries every 5 minutes and auto-deactivates the subscription after persistent failures (you'll get 2 emails). Call `webhooks.activate(id)` to bring it back online once your endpoint is healthy.
161
+
162
+ ### Handle a return / claim
163
+
164
+ ```ts
165
+ // 1. New customer-filed claims arrive via list().
166
+ const claims = await client.claims.list({ claimItemStatus: 'WaitingInAction' });
167
+
168
+ for (const claim of claims.items) {
169
+ // 2a. Approve all the line items in the claim → triggers refund flow.
170
+ await client.claims.approveLineItems(claim.id, {
171
+ claimLineItemIdList: claim.raw.items.map((i: any) => i.id),
172
+ });
173
+
174
+ // 2b. OR reject the claim with a documented reason + supporting docs.
175
+ const reasons = await client.claims.getIssueReasons();
176
+ await client.claims.createIssue(claim.id, {
177
+ claimIssueReasonId: reasons.find((r) => r.name.includes('kullanılmış'))!.id,
178
+ claimItemIdList: claim.raw.items.map((i: any) => i.id),
179
+ description: 'Ürün kullanılmış olarak iade edildi, retten kaynaklı reddediliyor.',
180
+ files: [pdfBlob, photoBlob],
181
+ });
182
+ }
183
+
184
+ // 3. After you've received the physical package back, mark it:
185
+ await client.orders.manualReturnByPackageId(packageId);
186
+ // or, if you only have the cargo tracking number:
187
+ await client.orders.manualReturnByTrackingNumber(trackingNumber);
188
+ ```
189
+
190
+ ### Reconcile settlements
191
+
192
+ ```ts
193
+ const start = new Date('2026-05-01');
194
+ const end = new Date('2026-05-31');
195
+
196
+ for await (const tx of paginate((p) =>
197
+ client.finance.getSettlements({ ...p, startDate: start, endDate: end }),
198
+ )) {
199
+ // tx is a typed FinancialTransaction — no .raw drill required for documented fields
200
+ if (tx.transactionType === 'Satış' && tx.orderNumber) {
201
+ await db.recordSale({
202
+ orderNumber: tx.orderNumber,
203
+ revenue: tx.sellerRevenue ?? 0,
204
+ commission: tx.commissionAmount ?? 0,
205
+ transactionDate: tx.transactionDate,
206
+ });
207
+ }
208
+ }
209
+
210
+ // "Other financials" (cargo deductions, labor adjustments) share the same shape.
211
+ const cargoDeductions = await client.finance.getOtherFinancials({
212
+ transactionType: 'DeductionInvoices',
213
+ });
214
+ ```
215
+
118
216
  ## Per-resource cheat sheet
119
217
 
120
218
  ```ts
@@ -140,25 +238,127 @@ await client.products.getBuyboxInfo(['BC1', 'BC2']); // max 10 per call
140
238
  await client.products.getBatchStatus(batchRequestId);
141
239
 
142
240
  // products — write (all return { batchRequestId }; max 1000 items)
143
- await client.products.create([...]);
241
+ await client.products.create([
242
+ /* CreateProductV2Input */
243
+ ]);
144
244
  await client.products.updateContent([{ contentId: 123, title: '...' }]);
145
245
  await client.products.updateVariants([{ barcode: 'BC1', stockCode: 'NEW' }]);
146
- await client.products.updateUnapproved([{ barcode: 'BC1', title: '...', /* fuller payload */ }]);
147
- await client.products.updateDeliveryInfo([{ barcode: 'BC1', deliveryOptions: { deliveryDuration: 3 } }]);
246
+ await client.products.updateUnapproved([{ barcode: 'BC1', title: '...' /* fuller payload */ }]);
247
+ await client.products.updateDeliveryInfo([
248
+ { barcode: 'BC1', deliveryOptions: { deliveryDuration: 3 } },
249
+ ]);
148
250
 
149
251
  // products — lifecycle
150
- await client.products.delete(['BC1']); // separately rate-limited (100/min)
151
- await client.products.archive(['BC1']); // PUT archived=true
152
- await client.products.unarchive(['BC1']); // PUT archived=false
153
- await client.products.unlock(['BC1']); // restore after Trendyol price-lock
252
+ await client.products.delete(['BC1']); // separately rate-limited (100/min)
253
+ await client.products.archive(['BC1']); // PUT archived=true
254
+ await client.products.unarchive(['BC1']); // PUT archived=false
255
+ await client.products.unlock(['BC1']); // restore after Trendyol price-lock
154
256
 
155
257
  // inventory — async batch
156
- await client.inventory.update([{ barcode: 'BC1', quantity: 50, salePrice: 199.9, listPrice: 299.9 }]);
258
+ await client.inventory.update([
259
+ { barcode: 'BC1', quantity: 50, salePrice: 199.9, listPrice: 299.9 },
260
+ ]);
157
261
 
158
- // orders — shipment packages
159
- for await (const pkg of paginate((p) => client.orders.list(p))) {
160
- console.log(pkg.id, pkg.status, pkg.lines.length);
161
- }
262
+ // orders — read
263
+ for await (const pkg of paginate((p) => client.orders.list(p))) { ... }
264
+ for await (const pkg of paginate((p) => client.orders.listStream({ ...p, packageItemStatuses: 'Created,Picking' }))) { ... }
265
+ await client.orders.getCargoInvoiceItems('INV-2026-001');
266
+
267
+ // orders — status / cargo
268
+ await client.orders.updatePackageStatus(pkgId, { status: 'Picking' });
269
+ await client.orders.updatePackageStatus(pkgId, { status: 'Invoiced' });
270
+ await client.orders.cancelPackageItem(pkgId, { lines: [{ lineId: 1, quantity: 1 }], reasonId: 577 });
271
+ await client.orders.extendDeliveryDate(pkgId, 2);
272
+ await client.orders.processAlternativeDelivery(pkgId, {
273
+ isPhoneNumber: false,
274
+ trackingInfo: 'https://my-cargo/track/abc',
275
+ params: { provider: 'EXAMPLE_CARGO' },
276
+ });
277
+
278
+ // orders — splits (4 variants — see JSDoc)
279
+ await client.orders.splitPackage(pkgId, [lineId1, lineId2]);
280
+ await client.orders.splitPackageByQuantity(pkgId, [{ orderLineId: 100, quantities: [2, 2, 1] }]);
281
+ await client.orders.multiSplitPackage(pkgId, [{ orderLineIds: [3, 5] }, { orderLineIds: [7, 8] }]);
282
+ await client.orders.splitMultiPackagesByQuantity(pkgId, [
283
+ { packageDetails: [{ orderLineId: 12345, quantities: 2 }] },
284
+ ]);
285
+
286
+ // orders — cargo + manual delivery
287
+ await client.orders.changeCargoProvider(pkgId, 'ARASMP'); // open enum (see TrendyolCargoProvider)
288
+ await client.orders.manualDeliverByPackageId(pkgId);
289
+ await client.orders.manualDeliverByTrackingNumber(trackingNumber);
290
+ await client.orders.markDeliveredByService(pkgId);
291
+
292
+ // orders — operational metadata
293
+ await client.orders.updateBoxInfo(pkgId, { deci: 2.5, boxQuantity: 1 });
294
+ await client.orders.updateLaborCosts(pkgId, [{ orderLineId: 100, laborCostPerItem: 32.12 }]);
295
+ await client.orders.updateWarehouse(pkgId, warehouseId);
296
+
297
+ // orders — returns + compensation
298
+ await client.orders.manualReturnByPackageId(pkgId);
299
+ await client.orders.manualReturnByTrackingNumber(trackingNumber);
300
+ const tickets = await client.orders.getCompensationTickets({ startDate: lastMonth }); // TEX-only
301
+
302
+ // claims
303
+ await client.claims.create({
304
+ orderNumber: 'ORD-1',
305
+ claimItems: [{ barcode: 'BC1', quantity: 1, reasonId: 401 }],
306
+ });
307
+ await client.claims.createIssue(claimId, {
308
+ claimIssueReasonId: 5,
309
+ claimItemIdList: ['item-1', 'item-2'],
310
+ description: '...',
311
+ files: [pdfBlob],
312
+ });
313
+ await client.claims.approveLineItems(claimId, { claimLineItemIdList: ['line-1'] });
314
+ const claims = await client.claims.list({ claimItemStatus: 'WaitingInAction' });
315
+ const reasons = await client.claims.getIssueReasons();
316
+ const audits = await client.claims.getItemAudits(claimItemId);
317
+
318
+ // webhooks
319
+ await client.webhooks.create({
320
+ url: 'https://my-app/hook',
321
+ authenticationType: 'API_KEY',
322
+ apiKey: 'rotatable-secret',
323
+ subscribedStatuses: ['CREATED', 'SHIPPED'],
324
+ });
325
+ const subs = await client.webhooks.list();
326
+ await client.webhooks.update(id, { ...updated });
327
+ await client.webhooks.delete(id);
328
+ await client.webhooks.activate(id);
329
+ await client.webhooks.deactivate(id);
330
+
331
+ // questions
332
+ const q = await client.questions.get(questionId);
333
+ const pending = await client.questions.list({ status: 'WAITING_FOR_ANSWER' });
334
+ await client.questions.answer(questionId, 'Cevap metni (10–2000 chars).');
335
+
336
+ // invoices
337
+ await client.invoices.uploadFile({ shipmentPackageId: 100, file: pdfBlob });
338
+ await client.invoices.sendLink({ shipmentPackageId: 100, invoiceLink: 'https://x/i.pdf' });
339
+ await client.invoices.deleteLink({ serviceSourceId: 1, channelId: 2, customerId: 3 });
340
+
341
+ // finance — typed FinancialTransaction[]
342
+ await client.finance.getSettlements({ startDate, endDate });
343
+ await client.finance.getOtherFinancials({ transactionType: 'DeductionInvoices' });
344
+
345
+ // labels
346
+ await client.labels.createCommon(trackingNumber, { format: 'ZPL', boxQuantity: 2 });
347
+ const label = await client.labels.getCommon(trackingNumber);
348
+ console.log(label.labels[0]?.label); // ZPL string
349
+
350
+ // test orders (STAGE-only)
351
+ await client.testOrders.create({
352
+ /* CreateTestOrderInput */
353
+ });
354
+ await client.testOrders.updateStatus(pkgId, 'Shipped');
355
+ await client.testOrders.setClaimsWaitingInAction();
356
+
357
+ // locations (no sellerId — utility lookup)
358
+ const countries = await client.locations.getCountries();
359
+ const cities = await client.locations.getTurkeyCities();
360
+ const districts = await client.locations.getTurkeyDistricts(cityCode);
361
+ const neighborhoods = await client.locations.getTurkeyNeighborhoods(cityCode, districtCode);
162
362
  ```
163
363
 
164
364
  ## Async batch + polling
@@ -175,7 +375,7 @@ const status = await client.products.getBatchStatus(batchRequestId);
175
375
 
176
376
  ## Discovery-first wire fixes
177
377
 
178
- `@lonca/trendyol` was built by hitting the live Trendyol STAGE for every endpoint before writing types. Several places where the official OpenAPI spec disagrees with the live wire are normalized automatically:
378
+ `@lonca/trendyol` was built by hitting the live Trendyol STAGE for every endpoint before writing types. Places where the official OpenAPI spec disagrees with the live wire are normalized automatically:
179
379
 
180
380
  - `categories.getAttributeValues`: spec says `attributeValueName`, wire returns `attributeValue` → SDK normalizes to `{ id, name }`
181
381
  - `products.listUnapproved`: spec says `media: [{url}]`, wire returns `images: [{url}]` → SDK exposes `images: string[]`
@@ -183,6 +383,12 @@ const status = await client.products.getBatchStatus(batchRequestId);
183
383
  - `products.updateUnapproved`: spec marks only `barcode` required, but live endpoint returns HTTP 500 (`TypeError`) when too many optional fields are omitted → documented in JSDoc
184
384
  - `brands.search`: docs claim case-sensitive exact match, live is substring + case-insensitive → documented in JSDoc
185
385
  - `getBatchRequestResult`: returns `PROCESSING + empty items` for unknown batch IDs (not 404)
386
+ - `orders.listStream` returns package ID as `id`, regular `orders.list` returns it as `shipmentPackageId` → normalizer accepts both
387
+ - `orders.updateLaborCosts`: body is a **raw array** (no `{ items: [...] }` envelope) — only endpoint in the surface that does this
388
+ - `getCompensationTickets`: spec says `{ data: { items: [] } }`, but SDK also accepts `{ data: [] }` and `{ content: [] }` defensively
389
+ - `labels.getCommon`: response is `{ data: [{ label, format }] }` → SDK surfaces `labels[]` for ergonomic access
390
+ - `finance.*`: both `getSettlements` and `getOtherFinancials` share the same `FinancialTransaction` wire schema → unified typed surface
391
+ - `webhooks.list`: SDK accepts 3 envelope shapes (`[]` raw, `{ webhooks: [] }`, `{ content: [] }`) and 3 active-flag spellings (`active`, `isActive`, `status: 'ACTIVE'`)
186
392
 
187
393
  Each fix is pinned by a regression mock test using the exact STAGE shape.
188
394
 
@@ -202,15 +408,31 @@ Trendyol uses HTTP Basic Auth. Get your `sellerId`, `apiKey`, and `apiSecret` fr
202
408
  ## Built-in robustness
203
409
 
204
410
  - **Retry with exponential backoff** on 429 (respects `Retry-After`) and 5xx
205
- - **Per-endpoint rate limiting** (token bucket) sized to Trendyol's documented limits — separate buckets for filter (2000/min), batch read (1000/min), buybox (1000/min), writes (1000/min), and delete (100/min)
411
+ - **Per-endpoint rate limiting** (token bucket) sized to Trendyol's documented limits — see defaults below; override per resource
412
+ - **Per-request correlation ID** — every call gets a UUID surfaced in log messages and the `x-correlationid` header for Trendyol-side log tracing
206
413
  - **Structured errors** via `@lonca/core` (`AuthError`, `RateLimitError`, `NotFoundError`, `ServerError`, `ValidationError`, `NetworkError`, `TimeoutError`)
207
- - **Client-side validation** before the network: empty batches, oversized batches (>1000 items), >10 buybox barcodes throw `ValidationError`
208
- - **Correlation ID** auto-generated per request for Trendyol-side log tracing
414
+ - **Client-side validation** before the network: empty batches, oversized batches (>1000 items), >10 buybox barcodes, ≤500-char claim descriptions, 10–2000-char Q&A answers — all throw `ValidationError`
415
+ - **Multipart upload support** — `claims.createIssue` and `invoices.uploadFile` build `FormData` internally and the transport handles `Content-Type` correctly
209
416
  - **`AbortSignal` support** throughout
210
417
 
418
+ ### Rate-limiter defaults
419
+
420
+ | Bucket | Default capacity | Interval | Used by |
421
+ | ------------ | :--------------: | :------: | --------------------------------------------- |
422
+ | `filter` | 2000 | 60 s | products filter / list |
423
+ | `batch read` | 1000 | 60 s | products batch read, orders list, finance |
424
+ | `buybox` | 1000 | 60 s | buybox lookups |
425
+ | `writes` | 1000 | 60 s | most write endpoints (create / update) |
426
+ | `delete` | 100 | 60 s | DELETE endpoints |
427
+ | `categories` | 50 | 60 s | categories list (cached by callers) |
428
+ | `webhooks` | 50 | 60 s | webhook config CRUD |
429
+ | `suppliers` | 1 | 1 h | suppliers list (Trendyol caps this at 1/hour) |
430
+
431
+ Override per resource by passing a `TokenBucketRateLimiter` from `@lonca/core` when constructing the resource directly.
432
+
211
433
  ## Stability
212
434
 
213
- `0.x` — alpha. The product surface is feature-complete and STAGE-verified, but public types may still adjust between minor versions until `1.0.0`.
435
+ `0.x` — alpha. The Trendyol surface is feature-complete and STAGE-verified, but public types may still adjust between minor versions until `1.0.0`.
214
436
 
215
437
  ## License
216
438