npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lonca/trendyol

v0.14.0

Published

Type-safe TypeScript SDK for the Trendyol Marketplace API

Readme

@lonca/trendyol

npm version

Type-safe TypeScript SDK for the Trendyol Marketplace API.

[!IMPORTANT] Unofficial. This is an independent, community-maintained SDK. It is not affiliated with, endorsed by, or supported by Trendyol. "Trendyol" and related names are trademarks of their respective owners.

Trendyol surface complete. 16 resources spanning catalog, orders, claims, finance, webhooks, and Export Center, plus a parseWebhookEvent helper for inbound event handling. Every endpoint a non-AutoFT non-V1 seller can hit is covered. See the npm badge above for the current release.

Coverage

Each entry is a method on the client.

| Resource | Methods | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | brands | list(), search(name) | | categories | list(), getAttributes(id), getAttributeValues(catId, attrId), getByBarcodes(barcodes) (AutoFT) | | suppliers | getAddresses({forceRefresh?}) (1-hour cache; rate-limited 1 req/hour on Trendyol) | | products read | list({...}), listInventoryAndPrice({...}) (lightweight stock + price), listUnapproved({...}), getBase(barcode), getBuyboxInfo(barcodes), getBatchStatus(id) | | products write | create(items), updateContent(items), updateVariants(items), updateUnapproved(items), updateDeliveryInfo(items) | | products life | delete(barcodes), archive(barcodes), unarchive(barcodes), unlock(barcodes) | | inventory | update(items) (stock + price, async batch) | | orders read | list({...}), listStream({...}) (opaque cursor for >10K), getCargoInvoiceItems(serial, {...}) | | orders write | updatePackageStatus(id, {...}), cancelPackageItem(id, {...}), extendDeliveryDate(id, 1\|2\|3), processAlternativeDelivery(id, {...}) | | orders split | splitPackage, splitPackageByQuantity, multiSplitPackage, splitMultiPackagesByQuantity (4 variants) | | orders cargo | changeCargoProvider(id, code), manualDeliverByPackageId(id), manualDeliverByTrackingNumber(trk), markDeliveredByService(id) | | orders ops | updateBoxInfo(id, {...}), updateLaborCosts(id, items), updateWarehouse(id, warehouseId) | | orders returns | manualReturnByPackageId(id), manualReturnByTrackingNumber(trk), getCompensationTickets({...}) (TEX) | | claims | create({...}), createIssue(id, {...}) (multipart), approveLineItems(id, {...}), list({...}), getIssueReasons(), getItemAudits(itemId) | | webhooks | create({...}), list(), update(id, {...}), delete(id), activate(id), deactivate(id) | | questions | get(id), list({...}), answer(id, text) | | invoices | uploadFile({shipmentPackageId, file, ...}) (multipart), sendLink({...}), deleteLink({...}) | | finance | getSettlements({...}), getOtherFinancials({...}) — both return typed FinancialTransaction[] | | labels | createCommon(trackingNumber, {format: 'ZPL', ...}), getCommon(trackingNumber) | | testOrders | create({...}), updateStatus(id, status), setClaimsWaitingInAction()STAGE-only utility | | locations | getCountries(), getTurkeyCities(), getTurkeyDistricts(cityCode), getTurkeyNeighborhoods(cityCode, districtCode), getAzerbaijanCities(), getAzerbaijanDistricts(...), getCitiesByCountry/getDistrictsByCity(...) | | exportCenter | listProducts({...}), createProducts(items), updatePrices(items), updateStocks(items), getBatchStatus(batchId), listPackagesV2/V3({...}), getPackageItems({packageId, ...}), getCategoryAttributes(id), getCareInstructions(), getCompositions(), getOrigins()Trendyol Export Center / İhracat Merkezi | | videos | create({contentId, url, ...}), list({id?, sellerIntegrationStatus?, ...}) — product-page video upload + status | | top-level | parseWebhookEvent(rawBody), normalizeShipmentPackage(rawNode) — for inbound webhook handlers |

Intentionally excluded: processAlternativeDeliveryDigital (digital products only). Note: V1 endpoint sunset is N/A — Trendyol's V2 docs serve the same paths as V1, and the SDK already emits the V2 response shape (nextPageToken pagination, content-based variants).

Install

pnpm add @lonca/trendyol @lonca/core
# or npm install / yarn add

@lonca/core is a peer dependency (error classes, the token-bucket limiter). The paginate / paginateOffset helpers and the CursorPage / OffsetPage types are re-exported from @lonca/trendyol, so you can import them straight from this package.

Quick start

import { createTrendyolClient, paginate } from '@lonca/trendyol';

const client = createTrendyolClient({
  sellerId: 12345,
  apiKey: process.env.TRENDYOL_API_KEY!,
  apiSecret: process.env.TRENDYOL_API_SECRET!,
  env: 'stage', // or 'prod'
  integratorName: 'MyCompany', // required; use 'SelfIntegration' if the seller owns the integration code
});

for await (const product of paginate((p) => client.products.list(p))) {
  for (const variant of product.variants) {
    console.log(variant.barcode, product.title, variant.stock ?? '?');
  }
}

End-to-end flows

Create a product

The chain brand → category → attributes (+ values) → addresses → create → poll → verify:

const [brand] = await client.brands.search('TRENDYOLMİLLA');
const tree = await client.categories.list();
const category = findLeaf(tree, /Elbise/); // your own walker

const attrs = await client.categories.getAttributes(category.id);
const renk = attrs.find((a) => a.name === 'Renk')!;
const renkValues = await client.categories.getAttributeValues(category.id, renk.id);
const kirmizi = renkValues.items.find((v) => v.name === 'Kırmızı')!;

const addresses = await client.suppliers.getAddresses();
const shipment = addresses.find((a) => a.isShipmentAddress)!;
const returning = addresses.find((a) => a.isReturningAddress)!;

const { batchRequestId } = await client.products.create([
  {
    barcode: 'MY-SKU-001',
    title: 'Kırmızı Elbise',
    productMainId: 'MY-MAIN-001',
    brandId: Number(brand.id),
    categoryId: Number(category.id),
    quantity: 10,
    stockCode: 'MY-SC-001',
    dimensionalWeight: 1,
    description: '<p>...</p>',
    listPrice: 299.9,
    salePrice: 199.9,
    images: [{ url: 'https://cdn.example.com/dress.jpg' }],
    vatRate: 20,
    attributes: [{ attributeId: Number(renk.id), attributeValueIds: [Number(kirmizi.id)] }],
    shipmentAddressId: Number(shipment.id),
    returningAddressId: Number(returning.id),
  },
]);

// Poll the batch → detect approval.
let result;
do {
  await new Promise((r) => setTimeout(r, 2000));
  result = await client.products.getBatchStatus(batchRequestId);
} while (result.items[0]?.status === 'PROCESSING');

if (result.items[0]?.status === 'SUCCESS') {
  const base = await client.products.getBase('MY-SKU-001');
  console.log('Approved:', base.approved, 'contentId:', base.contentId);
}

Handle inbound webhooks (Express)

Trendyol POSTs the same body shape as getShipmentPackages to your endpoint on status events. parseWebhookEvent returns typed ShipmentPackage[]:

import express from 'express';
import { parseWebhookEvent } from '@lonca/trendyol';

const app = express();

app.post('/trendyol/webhook', express.json(), (req, res) => {
  // Authenticate Trendyol against your endpoint here (Basic or x-api-key,
  // matching the auth method you configured on the subscription).

  const event = parseWebhookEvent(req.body);
  for (const pkg of event.packages) {
    // pkg: typed ShipmentPackage — same shape as orders.list()
    await myQueue.enqueue({
      packageId: pkg.id,
      orderNumber: pkg.orderNumber,
      status: pkg.status,
      createdBy: pkg.raw.createdBy, // 'order-creation' | 'cancel' | 'split' | 'transfer'
    });
  }
  res.sendStatus(200);
});

// Register the subscription once.
await client.webhooks.create({
  url: 'https://my-app.example.com/trendyol/webhook',
  authenticationType: 'API_KEY',
  apiKey: process.env.TRENDYOL_WEBHOOK_API_KEY!,
  subscribedStatuses: ['CREATED', 'SHIPPED', 'DELIVERED'],
});

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.

Handle a return / claim

// 1. New customer-filed claims arrive via list().
const claims = await client.claims.list({ claimItemStatus: 'WaitingInAction' });

for (const claim of claims.items) {
  // 2a. Approve all the line items in the claim → triggers refund flow.
  await client.claims.approveLineItems(claim.id, {
    claimLineItemIdList: claim.raw.items.map((i: any) => i.id),
  });

  // 2b. OR reject the claim with a documented reason + supporting docs.
  const reasons = await client.claims.getIssueReasons();
  await client.claims.createIssue(claim.id, {
    claimIssueReasonId: reasons.find((r) => r.name.includes('kullanılmış'))!.id,
    claimItemIdList: claim.raw.items.map((i: any) => i.id),
    description: 'Ürün kullanılmış olarak iade edildi, retten kaynaklı reddediliyor.',
    files: [pdfBlob, photoBlob],
  });
}

// 3. After you've received the physical package back, mark it:
await client.orders.manualReturnByPackageId(packageId);
// or, if you only have the cargo tracking number:
await client.orders.manualReturnByTrackingNumber(trackingNumber);

Reconcile settlements

const start = new Date('2026-05-01');
const end = new Date('2026-05-31');

for await (const tx of paginate((p) =>
  // `transactionType` is required (Trendyol 500s without it); `limit` is clamped to 500/1000.
  client.finance.getSettlements({ ...p, startDate: start, endDate: end, transactionType: 'Sale' }),
)) {
  // tx is a typed FinancialTransaction — no .raw drill required for documented fields
  if (tx.transactionType === 'Satış' && tx.orderNumber) {
    await db.recordSale({
      orderNumber: tx.orderNumber,
      revenue: tx.sellerRevenue ?? 0,
      commission: tx.commissionAmount ?? 0,
      transactionDate: tx.transactionDate,
    });
  }
}

// "Other financials" (cargo deductions, labor adjustments) share the same shape.
const cargoDeductions = await client.finance.getOtherFinancials({
  transactionType: 'DeductionInvoices',
});

Per-resource cheat sheet

// brands
await client.brands.list({ limit: 1000 });
await client.brands.search('TRENDYOLMİLLA'); // substring + case-insensitive

// categories
const tree = await client.categories.list();
const attrs = await client.categories.getAttributes(catId);
const values = await client.categories.getAttributeValues(catId, attrId);
await client.categories.getByBarcodes(['BC1', 'BC2']); // requires AutoFT enrollment

// suppliers (cached 1h)
await client.suppliers.getAddresses();
await client.suppliers.getAddresses({ forceRefresh: true });

// products — read
await client.products.list({ barcode: 'BC1' });
await client.products.listInventoryAndPrice({ status: 'onSale', limit: 100 }); // stock + price only
await client.products.listUnapproved({ limit: 50 });
await client.products.getBase('BC1');
await client.products.getBuyboxInfo(['BC1', 'BC2']); // max 10 per call
await client.products.getBatchStatus(batchRequestId);

// products — write (all return { batchRequestId }; max 1000 items)
await client.products.create([
  /* CreateProductV2Input */
]);
await client.products.updateContent([{ contentId: 123, title: '...' }]);
await client.products.updateVariants([{ barcode: 'BC1', stockCode: 'NEW' }]);
await client.products.updateUnapproved([{ barcode: 'BC1', title: '...' /* fuller payload */ }]);
await client.products.updateDeliveryInfo([
  { barcode: 'BC1', deliveryOptions: { deliveryDuration: 3 } },
]);

// products — lifecycle
await client.products.delete(['BC1']); // separately rate-limited (100/min)
await client.products.archive(['BC1']); // PUT archived=true
await client.products.unarchive(['BC1']); // PUT archived=false
await client.products.unlock(['BC1']); // restore after Trendyol price-lock

// inventory — async batch
await client.inventory.update([
  { barcode: 'BC1', quantity: 50, salePrice: 199.9, listPrice: 299.9 },
]);

// orders — read
for await (const pkg of paginate((p) => client.orders.list(p))) { ... }
for await (const pkg of paginate((p) => client.orders.listStream({ ...p, packageItemStatuses: 'Created,Picking' }))) { ... }
await client.orders.getCargoInvoiceItems('INV-2026-001');

// orders — status / cargo
await client.orders.updatePackageStatus(pkgId, { status: 'Picking' });
await client.orders.updatePackageStatus(pkgId, { status: 'Invoiced' });
await client.orders.cancelPackageItem(pkgId, { lines: [{ lineId: 1, quantity: 1 }], reasonId: 577 });
await client.orders.extendDeliveryDate(pkgId, 2);
await client.orders.processAlternativeDelivery(pkgId, {
  isPhoneNumber: false,
  trackingInfo: 'https://my-cargo/track/abc',
  params: { provider: 'EXAMPLE_CARGO' },
});

// orders — splits (4 variants — see JSDoc)
await client.orders.splitPackage(pkgId, [lineId1, lineId2]);
await client.orders.splitPackageByQuantity(pkgId, [{ orderLineId: 100, quantities: [2, 2, 1] }]);
await client.orders.multiSplitPackage(pkgId, [{ orderLineIds: [3, 5] }, { orderLineIds: [7, 8] }]);
await client.orders.splitMultiPackagesByQuantity(pkgId, [
  { packageDetails: [{ orderLineId: 12345, quantities: 2 }] },
]);

// orders — cargo + manual delivery
await client.orders.changeCargoProvider(pkgId, 'ARASMP'); // open enum (see TrendyolCargoProvider)
await client.orders.manualDeliverByPackageId(pkgId);
await client.orders.manualDeliverByTrackingNumber(trackingNumber);
await client.orders.markDeliveredByService(pkgId);

// orders — operational metadata
await client.orders.updateBoxInfo(pkgId, { deci: 2.5, boxQuantity: 1 });
await client.orders.updateLaborCosts(pkgId, [{ orderLineId: 100, laborCostPerItem: 32.12 }]);
await client.orders.updateWarehouse(pkgId, warehouseId);

// orders — returns + compensation
await client.orders.manualReturnByPackageId(pkgId);
await client.orders.manualReturnByTrackingNumber(trackingNumber);
const tickets = await client.orders.getCompensationTickets({ startDate: lastMonth }); // TEX-only

// claims
await client.claims.create({
  orderNumber: 'ORD-1',
  claimItems: [{ barcode: 'BC1', quantity: 1, reasonId: 401 }],
});
await client.claims.createIssue(claimId, {
  claimIssueReasonId: 5,
  claimItemIdList: ['item-1', 'item-2'],
  description: '...',
  files: [pdfBlob],
});
await client.claims.approveLineItems(claimId, { claimLineItemIdList: ['line-1'] });
const claims = await client.claims.list({ claimItemStatus: 'WaitingInAction' });
const reasons = await client.claims.getIssueReasons();
const audits = await client.claims.getItemAudits(claimItemId);

// webhooks
await client.webhooks.create({
  url: 'https://my-app/hook',
  authenticationType: 'API_KEY',
  apiKey: 'rotatable-secret',
  subscribedStatuses: ['CREATED', 'SHIPPED'],
});
const subs = await client.webhooks.list();
await client.webhooks.update(id, { ...updated });
await client.webhooks.delete(id);
await client.webhooks.activate(id);
await client.webhooks.deactivate(id);

// questions
const q = await client.questions.get(questionId);
const pending = await client.questions.list({ status: 'WAITING_FOR_ANSWER' });
await client.questions.answer(questionId, 'Cevap metni (10–2000 chars).');

// invoices
await client.invoices.uploadFile({ shipmentPackageId: 100, file: pdfBlob });
await client.invoices.sendLink({ shipmentPackageId: 100, invoiceLink: 'https://x/i.pdf' });
await client.invoices.deleteLink({ serviceSourceId: 1, channelId: 2, customerId: 3 });

// finance — typed FinancialTransaction[]
await client.finance.getSettlements({ startDate, endDate, transactionType: 'Sale' }); // transactionType required
await client.finance.getOtherFinancials({ transactionType: 'DeductionInvoices' });

// labels
await client.labels.createCommon(trackingNumber, { format: 'ZPL', boxQuantity: 2 });
const label = await client.labels.getCommon(trackingNumber);
console.log(label.labels[0]?.label); // ZPL string

// test orders (STAGE-only)
await client.testOrders.create({
  /* CreateTestOrderInput */
});
await client.testOrders.updateStatus(pkgId, 'Shipped');
await client.testOrders.setClaimsWaitingInAction();

// locations (no sellerId — utility lookup)
const countries = await client.locations.getCountries();
const cities = await client.locations.getTurkeyCities();
const districts = await client.locations.getTurkeyDistricts(cityCode);
const neighborhoods = await client.locations.getTurkeyNeighborhoods(cityCode, districtCode);

Async batch + polling

Every write endpoint (products.create, updateContent, updateVariants, updateUnapproved, updateDeliveryInfo, delete, archive, unarchive, unlock, inventory.update) is asynchronous: Trendyol accepts the batch and returns a { batchRequestId }. Poll the result with:

const status = await client.products.getBatchStatus(batchRequestId);
// status.status: 'PROCESSING' | 'COMPLETED' | 'FAILED'
// status.items[].status: per-item outcome

Important: Trendyol's overall batch status can lag at PROCESSING even after each items[].status has settled. Trust the per-item status, or re-read the affected products via list({ barcode }) / getBase(barcode) to verify the change landed. Batch results are retained for 4 hours on Trendyol's side.

Authentication

Trendyol uses HTTP Basic Auth. Get your sellerId, apiKey, and apiSecret from the Trendyol Partner Panel → Account Info → Integration Information (master-user only).

Production vs Stage have different credentials. Stage also requires IP whitelisting — register your CI/server IP with Trendyol support (0850 258 58 00). The SDK auto-sends the 5 mandatory headers (Authorization, x-clientip, x-correlationid, x-agentname, User-Agent).

Environments

| Env | Base URL | Notes | | ------- | --------------------------------- | --------------------------------------------- | | prod | https://apigw.trendyol.com | No IP whitelist | | stage | https://stageapigw.trendyol.com | IP whitelist required — call Trendyol support |

Built-in robustness

  • Idempotency-aware retry with exponential backoff — reads (GET) retry on 429 (honoring Retry-After), 5xx, and network/timeout errors. Writes (POST/PUT/DELETE) retry only on 429, which the server rejects before processing; ambiguous 5xx/network/timeout failures on a write are not replayed, so a transient error can't duplicate an order action or stock/price push. A Retry-After: 0 no longer collapses backoff to an immediate retry.
  • Per-endpoint rate limiting (token bucket) sized to Trendyol's documented limits — see defaults below; override per resource
  • Per-request correlation ID — every call gets a UUID surfaced in log messages and the x-correlationid header for Trendyol-side log tracing
  • Structured errors via @lonca/core (AuthError, RateLimitError, NotFoundError, ServerError, ValidationError, NetworkError, TimeoutError)
  • 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
  • Multipart upload supportclaims.createIssue and invoices.uploadFile build FormData internally and the transport handles Content-Type correctly
  • AbortSignal support throughout

Rate-limiter defaults

| Bucket | Default capacity | Interval | Used by | | ------------ | :--------------: | :------: | --------------------------------------------- | | filter | 2000 | 60 s | products filter / list | | batch read | 1000 | 60 s | products batch read, orders list, finance | | buybox | 1000 | 60 s | buybox lookups | | writes | 1000 | 60 s | most write endpoints (create / update) | | delete | 100 | 60 s | DELETE endpoints | | categories | 50 | 60 s | categories list (cached by callers) | | webhooks | 50 | 60 s | webhook config CRUD | | suppliers | 1 | 1 h | suppliers list (Trendyol caps this at 1/hour) |

Override per resource by passing a TokenBucketRateLimiter from @lonca/core when constructing the resource directly.

Stability

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.

License

MIT