Working with Product Cards
Complete guide to fetching, filtering, and paginating product cards using the getCardsList() method.
Note: The older
createCardsList()method is deprecated. UsegetCardsList()instead -- they are functionally identical.
Table of Contents
- Overview
- Basic Usage
- Understanding the Request Structure
- First Request vs Pagination
- Filtering Options
- Complete Pagination Example
- Common Mistakes
- Troubleshooting
- Best Practices
- Response Structure
- TN VED and OKPD2 Directories
- Additional GTIN
- Documents
Overview
The getCardsList() method retrieves a list of your product cards from Wildberries. It supports:
- Cursor-based pagination for fetching large datasets
- Advanced filtering by photos, text search, brands, categories, tags
- Sorting by update date
- Efficient batching (maximum 100 cards per request)
API Endpoint: POST /content/v2/get/cards/list
Rate Limit: 100 requests/minute with 600ms intervals (burst: 5 requests)
Important: Cards in trash are NOT returned by this method. Use getTrashedCards() to fetch trashed cards separately.
🚨 CRITICAL: Pagination Limit Restrictions
MAXIMUM: 100 cards per request - the API will reject larger values with ValidationError (HTTP 400).
Verified through testing (December 2024):
- ✅
limit: 10- Works perfectly - ✅
limit: 100- RECOMMENDED & MAXIMUM - Official Wildberries recommendation - ❌
limit: 1000- FAILS with ValidationError (HTTP 400) - ❌
limit: 5000- FAILS with ValidationError (HTTP 400)
Why this matters: Although undocumented in Wildberries API specification, the API enforces a strict limit of 100 cards maximum. Any value exceeding 100 will result in request rejection.
Correct approach: Always use limit: 100 with proper pagination (see examples below).
Basic Usage
Simplest Example - First 100 Cards
import { WildberriesSDK } from 'daytona-wildberries-typescript-sdk';
const sdk = new WildberriesSDK({
apiKey: process.env.WB_API_KEY!
});
// Get first 100 product cards
const response = await sdk.products.getCardsList({
settings: {
filter: {
withPhoto: -1 // All cards (with and without photos)
},
cursor: {
limit: 100
}
}
});
console.log(`Fetched: ${response.cards?.length ?? 0} cards`);
console.log(`Total in account: ${response.cursor?.total ?? 0} cards`);Understanding the Request Structure
Request Schema
{
settings: {
sort?: {
ascending?: boolean; // Sort by updatedAt (false = descending)
};
filter?: {
withPhoto?: number; // Photo filter: -1, 0, or 1
textSearch?: string; // Search by vendor code, nmID, barcode
tagIDs?: number[]; // Filter by tag IDs
allowedCategoriesOnly?: boolean; // Only allowed categories
objectIDs?: number[]; // Filter by subject IDs
brands?: string[]; // Filter by brand names
imtID?: number; // Filter by merged card ID
};
cursor: {
limit: number; // Cards per request (MAXIMUM: 100)
updatedAt?: string; // For pagination (ISO 8601 timestamp)
nmID?: number; // For pagination (Wildberries article ID)
};
}
}Query Parameters
{
locale?: 'ru' | 'en' | 'zh'; // Language for name, value, object fields
}First Request vs Pagination
🔑 Critical Difference
FIRST REQUEST (getting initial batch):
// ✅ CORRECT - Only specify limit
{
settings: {
cursor: {
limit: 100 // ONLY limit, DO NOT include updatedAt or nmID
},
filter: { withPhoto: -1 }
}
}// ❌ WRONG - Empty values cause validation errors
{
settings: {
cursor: {
limit: 100,
updatedAt: "", // ❌ Remove this for first request
nmID: 0 // ❌ Remove this for first request
},
filter: { withPhoto: -1 }
}
}PAGINATION REQUESTS (getting next batches):
// ✅ CORRECT - Copy updatedAt and nmID from previous response
{
settings: {
cursor: {
limit: 100,
updatedAt: "2023-12-06T11:17:00.96577Z", // From response.cursor
nmID: 370870300 // From response.cursor
},
filter: { withPhoto: -1 }
}
}How Pagination Works
- Make first request with only
limitin cursor - Receive response with cards and cursor data
- Copy
cursor.updatedAtandcursor.nmIDfrom response - Paste into next request's cursor
- Repeat until
cursor.total < limitorcards.length < limit
Filtering Options
Photo Filter
// All cards (with and without photos) - DEFAULT
withPhoto: -1
// Only cards WITHOUT photos
withPhoto: 0
// Only cards WITH photos
withPhoto: 1Text Search (Vendor Code, nmID, Barcode)
{
settings: {
filter: {
textSearch: '4603743187500888', // Searches in vendor code, nmID, barcode
withPhoto: -1
},
cursor: { limit: 100 }
}
}Filter by Brand
{
settings: {
filter: {
brands: ['Nike', 'Adidas', 'Puma'],
withPhoto: -1
},
cursor: { limit: 100 }
}
}Filter by Tag IDs
// First, get available tags
const tags = await sdk.products.getContentTags();
console.log(tags);
// Then filter by specific tag IDs
{
settings: {
filter: {
tagIDs: [345, 415], // Tag IDs from getContentTags()
withPhoto: -1
},
cursor: { limit: 100 }
}
}Filter by Subject (Category)
{
settings: {
filter: {
objectIDs: [235, 67], // Subject IDs
withPhoto: -1
},
cursor: { limit: 100 }
}
}Filter by Merged Card ID
{
settings: {
filter: {
imtID: 328632, // Get all variants of merged card
withPhoto: -1
},
cursor: { limit: 100 }
}
}Combine Multiple Filters
{
settings: {
filter: {
brands: ['Nike'],
objectIDs: [235],
withPhoto: 1, // Only with photos
tagIDs: [345]
},
cursor: { limit: 100 }
}
}Complete Pagination Example
Fetch All Product Cards with Pagination
async function getAllProductCards() {
const allCards = [];
let hasMore = true;
let cursor: any = { limit: 100 }; // Start with only limit
while (hasMore) {
const response = await sdk.products.getCardsList({
settings: {
filter: { withPhoto: -1 },
cursor
}
});
// Add cards to result
if (response.cards) {
allCards.push(...response.cards);
}
// Check if more data available
const receivedCount = response.cards?.length ?? 0;
// Stop if we received less than limit (last page)
if (receivedCount < 100) {
hasMore = false;
} else if (response.cursor) {
// Update cursor for next request
cursor = {
limit: 100,
updatedAt: response.cursor.updatedAt,
nmID: response.cursor.nmID
};
} else {
hasMore = false;
}
console.log(`Progress: ${allCards.length} cards fetched`);
// Optional: Add delay to respect rate limits
await new Promise(resolve => setTimeout(resolve, 650));
}
console.log(`Total cards fetched: ${allCards.length}`);
return allCards;
}
// Usage
const allCards = await getAllProductCards();Pagination with Specific Filters
async function getFilteredCardsWithPagination(filters: {
brands?: string[];
withPhoto?: number;
textSearch?: string;
}) {
const allCards = [];
let cursor: any = { limit: 100 };
while (true) {
const response = await sdk.products.getCardsList({
settings: {
filter: {
...filters,
withPhoto: filters.withPhoto ?? -1
},
cursor
}
});
if (response.cards) {
allCards.push(...response.cards);
}
const receivedCount = response.cards?.length ?? 0;
if (receivedCount < 100 || !response.cursor?.updatedAt) {
break;
}
cursor = {
limit: 100,
updatedAt: response.cursor.updatedAt,
nmID: response.cursor.nmID
};
console.log(`Fetched ${allCards.length} cards...`);
await new Promise(resolve => setTimeout(resolve, 650));
}
return allCards;
}
// Usage examples
const nikeCards = await getFilteredCardsWithPagination({
brands: ['Nike'],
withPhoto: 1
});
const cardsWithPhotos = await getFilteredCardsWithPagination({
withPhoto: 1
});Common Mistakes
❌ Mistake 1: Including Empty Cursor Fields in First Request
// ❌ WRONG - Causes "Validation failed"
const response = await sdk.products.getCardsList({
settings: {
cursor: {
limit: 100,
updatedAt: "", // Empty string causes validation error
nmID: 0 // Zero value causes validation error
},
filter: { withPhoto: -1 }
}
});✅ Solution: Omit updatedAt and nmID in first request:
const response = await sdk.products.getCardsList({
settings: {
cursor: {
limit: 100 // ONLY limit
},
filter: { withPhoto: -1 }
}
});❌ Mistake 2: Missing settings Wrapper
// ❌ WRONG - Missing settings wrapper
const response = await sdk.products.getCardsList({
cursor: { limit: 100 }, // Should be inside settings
filter: { withPhoto: -1 } // Should be inside settings
});✅ Solution: Wrap everything in settings:
const response = await sdk.products.getCardsList({
settings: { // ✅ Wrapper required
cursor: { limit: 100 },
filter: { withPhoto: -1 }
}
});❌ Mistake 3: Exceeding Limit Maximum (CRITICAL)
// ❌ WRONG - Limit exceeds maximum, causes ValidationError (HTTP 400)
const response = await sdk.products.getCardsList({
settings: {
cursor: { limit: 1000 } // ❌ FAILS - Maximum is 100!
}
});
// ❌ ALSO WRONG - Even higher values fail
const response = await sdk.products.getCardsList({
settings: {
cursor: { limit: 5000 } // ❌ FAILS - Maximum is 100!
}
});✅ Solution: ALWAYS use limit: 100 (maximum allowed):
const response = await sdk.products.getCardsList({
settings: {
cursor: { limit: 100 } // ✅ MAXIMUM & RECOMMENDED
}
});Error you'll see if exceeded:
ValidationError: Validation failed
HTTP Status: 400❌ Mistake 4: Not Handling Pagination
// ❌ WRONG - Only gets first 100 cards
const response = await sdk.products.getCardsList({
settings: {
cursor: { limit: 100 },
filter: { withPhoto: -1 }
}
});
// If you have 500 cards, you missed 400!✅ Solution: Implement pagination loop (see Complete Pagination Example)
❌ Mistake 5: Incorrect API Key Permissions
// ❌ WRONG - Using API key without "Content" or "Promotion" category✅ Solution: Create API key with proper permissions:
- Go to Seller Portal → Settings → API Keys
- Create new key with "Контент" or "Продвижение" category
Troubleshooting
Error: "Validation failed"
Symptoms:
- Request returns 400 status code
- Error message: "Validation failed"
Common Causes & Solutions:
Empty cursor fields in first request
typescript// ❌ Problem cursor: { limit: 100, updatedAt: "", nmID: 0 } // ✅ Solution cursor: { limit: 100 }Limit exceeds maximum (MOST COMMON)
typescript// ❌ Problem - API returns ValidationError (HTTP 400) cursor: { limit: 1000 } // ❌ Exceeds maximum! cursor: { limit: 5000 } // ❌ Exceeds maximum! // ✅ Solution - Use maximum allowed value cursor: { limit: 100 } // ✅ MAXIMUM: 100 cardsNote: This is the #1 cause of ValidationError. The API strictly enforces a 100-card limit despite incomplete documentation.
Missing settings wrapper
typescript// ❌ Problem { cursor: { limit: 100 } } // ✅ Solution { settings: { cursor: { limit: 100 } } }
Error: 401 Unauthorized or 403 Forbidden
Cause: API key lacks required permissions
Solution:
- Go to Wildberries Seller Portal
- Navigate to Settings → API Keys
- Create new key with "Контент" or "Продвижение" category
- Update
WB_API_KEYenvironment variable
Error: 429 Too Many Requests
Cause: Rate limit exceeded (100 requests/minute)
Solution:
// Add delay between requests
async function fetchWithDelay() {
const response = await sdk.products.getCardsList({...});
// Wait 650ms before next request (100 req/min = 600ms interval + buffer)
await new Promise(resolve => setTimeout(resolve, 650));
return response;
}Getting 0 Cards When You Have Products
Possible Causes:
Cards are in trash
typescript// Use separate method for trashed cards const trashedCards = await sdk.products.getTrashedCards({ settings: { cursor: { limit: 100 } } });Wrong filter applied
typescript// Check filter settings filter: { withPhoto: 0 } // Only returns cards WITHOUT photosText search too specific
typescript// Try broader search or remove filter filter: { textSearch: '' } // Empty search = no filter
Response Has No cursor Field
Cause: This is the last page of results
Solution: This is expected behavior when you've fetched all cards. The pagination loop should exit.
if (!response.cursor?.updatedAt) {
console.log('Last page reached');
break;
}Best Practices
1. ALWAYS Use Maximum Allowed Batch Size
// ✅ CORRECT - Maximum allowed by API
cursor: { limit: 100 }
// ❌ FAILS - Exceeds maximum, causes ValidationError (HTTP 400)
cursor: { limit: 500 }
// ❌ FAILS - Exceeds maximum, causes ValidationError (HTTP 400)
cursor: { limit: 1000 }Why 100? The Wildberries API strictly enforces a maximum of 100 cards per request. Use pagination to fetch all cards.
2. Implement Rate Limit Protection
async function fetchWithRateLimit(requestFn: () => Promise<any>) {
const response = await requestFn();
// Wait 650ms between requests (100 req/min = 600ms + buffer)
await new Promise(resolve => setTimeout(resolve, 650));
return response;
}3. Handle Errors Gracefully
async function safeFetchCards(cursor: any, retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await sdk.products.getCardsList({
settings: { cursor, filter: { withPhoto: -1 } }
});
} catch (error: any) {
if (error.statusCode === 429 && attempt < retries) {
console.log(`Rate limited, waiting 60s (attempt ${attempt}/${retries})`);
await new Promise(resolve => setTimeout(resolve, 60000));
} else if (error.statusCode === 401 || error.statusCode === 403) {
throw new Error('Invalid API key or insufficient permissions');
} else if (attempt === retries) {
throw error;
}
}
}
}4. Log Progress for Large Datasets
async function getAllCardsWithProgress() {
const allCards = [];
let cursor: any = { limit: 100 };
let pageNumber = 1;
while (true) {
console.log(`Fetching page ${pageNumber}...`);
const response = await sdk.products.getCardsList({
settings: { cursor, filter: { withPhoto: -1 } }
});
if (response.cards) {
allCards.push(...response.cards);
console.log(` → Received ${response.cards.length} cards`);
console.log(` → Total so far: ${allCards.length}`);
console.log(` → Account total: ${response.cursor?.total ?? 'unknown'}`);
}
if ((response.cards?.length ?? 0) < 100 || !response.cursor?.updatedAt) {
break;
}
cursor = {
limit: 100,
updatedAt: response.cursor.updatedAt,
nmID: response.cursor.nmID
};
pageNumber++;
await new Promise(resolve => setTimeout(resolve, 650));
}
console.log(`✅ Completed: ${allCards.length} cards fetched`);
return allCards;
}5. Cache Results When Appropriate
import { writeFileSync, readFileSync, existsSync } from 'fs';
async function getCachedCards(cacheDuration = 3600000) { // 1 hour
const cacheFile = 'cards-cache.json';
if (existsSync(cacheFile)) {
const cache = JSON.parse(readFileSync(cacheFile, 'utf-8'));
const age = Date.now() - cache.timestamp;
if (age < cacheDuration) {
console.log('Using cached cards');
return cache.cards;
}
}
console.log('Fetching fresh cards from API');
const cards = await getAllProductCards();
writeFileSync(cacheFile, JSON.stringify({
timestamp: Date.now(),
cards
}));
return cards;
}6. Filter Early to Reduce Data Transfer
// ✅ GOOD - Filter on API side
const nikeCards = await sdk.products.getCardsList({
settings: {
filter: { brands: ['Nike'] },
cursor: { limit: 100 }
}
});
// ❌ INEFFICIENT - Fetch all, then filter client-side
const allCards = await getAllProductCards();
const nikeCards = allCards.filter(c => c.brand === 'Nike');Response Structure
Response Type
{
cards?: Array<{
nmID?: number; // Wildberries article ID
imtID?: number; // Merged card ID (same for all variants)
nmUUID?: string; // Internal technical ID (UUID)
subjectID?: number; // Subject (category) ID
subjectName?: string; // Subject name
vendorCode?: string; // Your vendor code (SKU)
brand?: string; // Brand name
title?: string; // Product title
description?: string; // Product description
needKiz?: boolean; // Requires marking code (честныйзнак.рф)
photos?: Array<{
big?: string; // Large photo URL
c246x328?: string; // 246x328px photo URL
c516x688?: string; // 516x688px photo URL
square?: string; // Square photo URL
tm?: string; // Thumbnail URL
}>;
video?: string; // Video URL
wholesale?: {
enabled?: boolean; // Wholesale enabled
quantum?: number; // Minimum wholesale quantity
};
dimensions?: {
length?: number; // Length in cm
width?: number; // Width in cm
height?: number; // Height in cm
weightBrutto?: number; // Weight in kg
isValid?: boolean; // Dimensions validated
};
characteristics?: Array<{
id?: number; // Characteristic ID
name?: string; // Characteristic name
value?: any; // Characteristic value (can be string, number, array)
}>;
sizes?: Array<{
chrtID?: number; // Size chart ID
techSize?: string; // Technical size
wbSize?: string; // Display size
skus?: string[]; // Barcodes for this size
}>;
tags?: Array<{
id?: number; // Tag ID
name?: string; // Tag name
color?: string; // Tag color (hex)
}>;
createdAt?: string; // Creation date (ISO 8601)
updatedAt?: string; // Last update date (ISO 8601)
}>;
cursor?: {
updatedAt?: string; // Copy to next request for pagination
nmID?: number; // Copy to next request for pagination
total?: number; // Total cards in account (informational)
};
}Example Response
{
"cards": [
{
"nmID": 123456789,
"imtID": 328632,
"vendorCode": "MY-PRODUCT-001",
"brand": "MyBrand",
"title": "Premium Product Title",
"description": "Detailed product description",
"subjectID": 235,
"subjectName": "Shirts",
"photos": [
{
"big": "https://basket-01.wb.ru/vol123/part456/123456789/images/big/1.jpg",
"c516x688": "https://basket-01.wb.ru/vol123/part456/123456789/images/c516x688/1.jpg"
}
],
"sizes": [
{
"chrtID": 987654,
"techSize": "XL",
"wbSize": "XL",
"skus": ["4603743187500888"]
}
],
"updatedAt": "2023-12-06T11:17:00.96577Z"
}
],
"cursor": {
"updatedAt": "2023-12-06T11:17:00.96577Z",
"nmID": 123456789,
"total": 500
}
}TN VED and OKPD2 Directories
The three classification-directory methods use a Content API token and share the Content API limit of 100 requests per minute, a 600 ms interval, and a burst of 5 requests.
const tnved = await sdk.products.getDirectoryTnvedAll({
search: 6106,
locale: 'en',
});
const subjectOkpd = await sdk.products.getDirectoryOkpd({
subjectId: 105,
search: '61.06',
locale: 'ru',
});
const allOkpd = await sdk.products.getDirectoryOkpdAll({
search: 61.06,
locale: 'ru',
});The official raw Swagger permits ru only for both OKPD2 methods. On the same date, the rendered portal page showed ru and en for the full directory. The SDK follows the exact machine-readable request contract, but this source conflict is not proof that the WB runtime rejects en. TN VED permits both ru and en.
The raw schema declares getDirectoryOkpdAll().search as a number, even though its prose describes dotted text prefixes. The SDK preserves that numeric type. By comparison, getDirectoryOkpd().search is a string, so pass a dotted prefix such as 61.06 as a string.
Additional GTIN
The optional gtin field is placed at card level, not size level. It is a non-null string when present at all four contract locations:
| Operation | Field location |
|---|---|
createCardsUpload() | variants[].gtin |
createCardsUpdate() | each update item's gtin |
createUploadAdd() | cardsToAdd[].gtin |
getCardsList() | response cards[].gtin, when populated |
const historicalGtin = '04600000000018';
await sdk.products.createCardsUpload([{
subjectID: 105,
variants: [{
vendorCode: 'ART-GTIN-CREATE',
gtin: historicalGtin,
sizes: [{ skus: ['04600000000995'] }],
}],
}]);
await sdk.products.createCardsUpdate([{
nmID: 12345678,
vendorCode: 'ART-GTIN-UPDATE',
gtin: historicalGtin,
sizes: [{ skus: ['04600000000996'] }],
}]);
await sdk.products.createUploadAdd({
imtID: 98765,
cardsToAdd: [{
vendorCode: 'ART-GTIN-ADD',
gtin: historicalGtin,
sizes: [{ skus: ['04600000000997'] }],
}],
});
const cards = await sdk.products.getCardsList({
settings: { cursor: { limit: 100 } },
});
const listedGtin: string | undefined = cards.cards?.[0]?.gtin;WB accepts an additional GTIN only when the same value previously occurred in skus of one of the seller's listings; it does not have to be the same listing. This depends on remote seller history and is enforced by WB, so the SDK passes the field without local history validation. Current skus values must remain unique across listings and sizes. The example therefore keeps the historical GTIN out of all current skus arrays and uses a different current SKU in each payload. The pinned contract makes this feature available only to sellers registered in Russia. Omit gtin when it is not needed.
Documents
Since v4.3.0 / WB news 2026-09. Product cards support a dedicated
documentsobject increateCardsUpload()(per variant),createUploadAdd()(per card to add),createCardsUpdate(), and ingetCardsList()responses (with validation verdicts).
Document Types
| Type | Document |
|---|---|
1 | Certificate of Conformity |
2 | Correspondence (Conformity) Declaration |
3 | Certificate of Registration of SGR |
4 | Registration Certificate |
5 | Certificate of Registration of the Republic of Belarus |
7 | Registration of a Pesticide |
8 | Registration of an Agrochemical |
9 | Medicine Registration Certificate |
Note: type 6 does not exist in the WB spec.
Passing Documents on Create
const result = await sdk.products.createCardsUpload([
{
subjectID: 105,
variants: [
{
vendorCode: 'ART-001',
brand: 'MyBrand',
title: 'Product Name',
sizes: [{ techSize: '42', skus: ['1234567890123'] }],
characteristics: [{ id: 1, value: 'Blue' }],
documents: {
items: [
{
type: 1, // Certificate of Conformity
number: 'RU D-RU.АГ01.В.12345',
tradeName: 'Product Name',
startDate: '2025-01-15T00:00:00Z',
endDate: '2028-01-14T23:59:59Z',
isEndless: false,
},
],
excludeDocuments: false,
},
},
],
},
]);excludeDocuments: true skips document checks during listing validation — but all values passed to documents are then replaced with empty values. Leave it false unless you deliberately want to opt out of validation.
⚠️ Do Not Pass Documents via Characteristics
Passing documents through the characteristics array is now restricted: it keeps working only if the documents object was never used for the card AND the "Documents" block in the new WB seller cabinet was never filled. Otherwise characteristics-based documents may be processed incorrectly. WB recommends using the documents object only.
⚠️ Update Overwrites the Card — Resend ALL Documents
createCardsUpdate() fully overwrites the card. Pass ALL documents, including the unchanged ones, or the omitted documents are dropped. Reuse the id of an existing document (from getCardsList() → cards[].documents.items[].id) to keep it:
// 1. Fetch current documents
const list = await sdk.products.getCardsList({
settings: { cursor: { limit: 100 }, filter: { nmID: 12345678 } },
});
const docs = list.cards[0].documents?.items ?? [];
// 2. Update the card resending every document (unchanged ones keep their id)
await sdk.products.createCardsUpdate([
{
nmID: 12345678,
vendorCode: 'ART-001',
sizes: [{ chrtID: 111, techSize: '42', wbSize: '42', skus: ['1234567890123'] }],
documents: {
items: [
...docs.map(({ verdict: _verdict, createdAt: _createdAt, ...doc }) => doc),
{ type: 3, number: 'RU.77.99.88.001.E.002000.01.20', isEndless: true }, // new document
],
excludeDocuments: false,
},
},
]);Reading Validation Verdicts
getCardsList() returns each document with a verdict (once validation completes), plus an overallVerdict for the whole listing. status: 1 = passed, status: 2 = rejected — rejected verdicts carry a machine-readable reason (CardDocumentReason for documents, CardListingValidationReason for the listing):
const list = await sdk.products.getCardsList({ settings: { cursor: { limit: 100 } } });
for (const card of list.cards ?? []) {
for (const doc of card.documents?.items ?? []) {
if (doc.verdict?.status === 2) {
console.error(
`Card ${card.nmID}: document ${doc.number} rejected — ${doc.verdict.reason}`
);
// e.g. 'document_expired', 'document_not_found', 'applicant_mismatch', ...
}
}
if (card.documents?.overallVerdict?.status === 2) {
console.error(`Card ${card.nmID}: listing rejected — ${card.documents.overallVerdict.reason}`);
// e.g. 'tnved_missing', 'kiz_required', ...
}
}Documents previously passed only via characteristics are auto-duplicated into the documents object by WB, so the response always shows the full document set.
Related Resources
- Mandatory Product Characteristics - Required characteristics for card creation in 10+ categories — enforcement began April 29, 2026. Check
isRequiredForCreateongetObjectCharc()before creating cards. - Product Catalog Use Case - Complete product catalog sync examples
- Stock Management Guide - Managing inventory with fetched cards
- Best Practices Guide - General SDK best practices
- API Reference: ProductsModule - TypeScript API documentation
Summary
Key Takeaways:
- ✅ First request: Only include
limitin cursor - ✅ Pagination: Copy
updatedAtandnmIDfrom response cursor - ✅ Always wrap parameters in
settingsobject - ✅ Use
limit: 100for optimal performance - ✅ Implement rate limiting (650ms between requests)
- ✅ Handle errors and retry on transient failures
- ✅ Check API key has "Content" or "Promotion" permissions
Quick Reference:
// First request
const first = await sdk.products.getCardsList({
settings: {
cursor: { limit: 100 },
filter: { withPhoto: -1 }
}
});
// Next request (pagination)
const next = await sdk.products.getCardsList({
settings: {
cursor: {
limit: 100,
updatedAt: first.cursor.updatedAt,
nmID: first.cursor.nmID
},
filter: { withPhoto: -1 }
}
});Need help? Check the Troubleshooting Guide or open an issue.