Skip to content

Working with Product Cards ​

Complete guide to fetching, filtering, and paginating product cards using the getCardsList() method.

Note: The older createCardsList() method is deprecated. Use getCardsList() instead -- they are functionally identical.

Table of Contents ​


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 ​

typescript
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 ​

typescript
{
  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 ​

typescript
{
  locale?: 'ru' | 'en' | 'zh';  // Language for name, value, object fields
}

First Request vs Pagination ​

🔑 Critical Difference ​

FIRST REQUEST (getting initial batch):

typescript
// ✅ CORRECT - Only specify limit
{
  settings: {
    cursor: {
      limit: 100  // ONLY limit, DO NOT include updatedAt or nmID
    },
    filter: { withPhoto: -1 }
  }
}
typescript
// ❌ 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):

typescript
// ✅ 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 ​

  1. Make first request with only limit in cursor
  2. Receive response with cards and cursor data
  3. Copy cursor.updatedAt and cursor.nmID from response
  4. Paste into next request's cursor
  5. Repeat until cursor.total < limit or cards.length < limit

Filtering Options ​

Photo Filter ​

typescript
// All cards (with and without photos) - DEFAULT
withPhoto: -1

// Only cards WITHOUT photos
withPhoto: 0

// Only cards WITH photos
withPhoto: 1

Text Search (Vendor Code, nmID, Barcode) ​

typescript
{
  settings: {
    filter: {
      textSearch: '4603743187500888',  // Searches in vendor code, nmID, barcode
      withPhoto: -1
    },
    cursor: { limit: 100 }
  }
}

Filter by Brand ​

typescript
{
  settings: {
    filter: {
      brands: ['Nike', 'Adidas', 'Puma'],
      withPhoto: -1
    },
    cursor: { limit: 100 }
  }
}

Filter by Tag IDs ​

typescript
// 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) ​

typescript
{
  settings: {
    filter: {
      objectIDs: [235, 67],  // Subject IDs
      withPhoto: -1
    },
    cursor: { limit: 100 }
  }
}

Filter by Merged Card ID ​

typescript
{
  settings: {
    filter: {
      imtID: 328632,  // Get all variants of merged card
      withPhoto: -1
    },
    cursor: { limit: 100 }
  }
}

Combine Multiple Filters ​

typescript
{
  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 ​

typescript
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 ​

typescript
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 ​

typescript
// ❌ 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:

typescript
const response = await sdk.products.getCardsList({
  settings: {
    cursor: {
      limit: 100  // ONLY limit
    },
    filter: { withPhoto: -1 }
  }
});

❌ Mistake 2: Missing settings Wrapper ​

typescript
// ❌ 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:

typescript
const response = await sdk.products.getCardsList({
  settings: {  // ✅ Wrapper required
    cursor: { limit: 100 },
    filter: { withPhoto: -1 }
  }
});

❌ Mistake 3: Exceeding Limit Maximum (CRITICAL) ​

typescript
// ❌ 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):

typescript
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 ​

typescript
// ❌ 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 ​

typescript
// ❌ 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:

  1. Empty cursor fields in first request

    typescript
    // ❌ Problem
    cursor: { limit: 100, updatedAt: "", nmID: 0 }
    
    // ✅ Solution
    cursor: { limit: 100 }
  2. 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 cards

    Note: This is the #1 cause of ValidationError. The API strictly enforces a 100-card limit despite incomplete documentation.

  3. 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:

  1. Go to Wildberries Seller Portal
  2. Navigate to Settings → API Keys
  3. Create new key with "Контент" or "Продвижение" category
  4. Update WB_API_KEY environment variable

Error: 429 Too Many Requests ​

Cause: Rate limit exceeded (100 requests/minute)

Solution:

typescript
// 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:

  1. Cards are in trash

    typescript
    // Use separate method for trashed cards
    const trashedCards = await sdk.products.getTrashedCards({
      settings: { cursor: { limit: 100 } }
    });
  2. Wrong filter applied

    typescript
    // Check filter settings
    filter: { withPhoto: 0 }  // Only returns cards WITHOUT photos
  3. Text 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.

typescript
if (!response.cursor?.updatedAt) {
  console.log('Last page reached');
  break;
}

Best Practices ​

1. ALWAYS Use Maximum Allowed Batch Size ​

typescript
// ✅ 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 ​

typescript
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 ​

typescript
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 ​

typescript
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 ​

typescript
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 ​

typescript
// ✅ 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 ​

typescript
{
  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 ​

json
{
  "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.

typescript
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:

OperationField location
createCardsUpload()variants[].gtin
createCardsUpdate()each update item's gtin
createUploadAdd()cardsToAdd[].gtin
getCardsList()response cards[].gtin, when populated
typescript
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 documents object in createCardsUpload() (per variant), createUploadAdd() (per card to add), createCardsUpdate(), and in getCardsList() responses (with validation verdicts).

Document Types ​

TypeDocument
1Certificate of Conformity
2Correspondence (Conformity) Declaration
3Certificate of Registration of SGR
4Registration Certificate
5Certificate of Registration of the Republic of Belarus
7Registration of a Pesticide
8Registration of an Agrochemical
9Medicine Registration Certificate

Note: type 6 does not exist in the WB spec.

Passing Documents on Create ​

typescript
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:

typescript
// 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):

typescript
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.



Summary ​

Key Takeaways:

  1. ✅ First request: Only include limit in cursor
  2. ✅ Pagination: Copy updatedAt and nmID from response cursor
  3. ✅ Always wrap parameters in settings object
  4. ✅ Use limit: 100 for optimal performance
  5. ✅ Implement rate limiting (650ms between requests)
  6. ✅ Handle errors and retry on transient failures
  7. ✅ Check API key has "Content" or "Promotion" permissions

Quick Reference:

typescript
// 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.

← Back to Guides | Next: Stock Management →

Made with ❤️ for the Wildberries developer community