Skip to content

Seller Analytics CSV Reports ​

This guide covers generating and downloading long-term analytics reports as CSV files through the Wildberries SDK. CSV reports let you export up to one year of historical data -- sales funnels, stock history, and search query analytics -- as ZIP archives containing CSV files.

Data freshness from 2026-09-24 ​

Wildberries refreshes data once every two hours for getSalesFunnelProducts(), getSalesFunnelProductsHistory() and getSalesFunnelGroupedHistory(), and for the DETAIL_HISTORY_REPORT and GROUPED_HISTORY_REPORT CSV exports created with createNmReportDownload().

This is the data refresh cadence, not a request-rate limit. Polling or generating another CSV sooner does not guarantee newer metrics. Order Feed (getOrderFeed()) can replace only order and buyout data; it does not replace views, cart additions or conversion metrics.

Sources: Sales Funnel, CSV reports.

Search Queries data freshness from 2026-10-12 ​

Wildberries refreshes data once every two hours for these five API operations and three CSV exports:

API methodEndpoint
createSearchReportReport()POST /api/v2/search-report/report
createTableGroup()POST /api/v2/search-report/table/groups
createTableDetail()POST /api/v2/search-report/table/details
createProductSearchText()POST /api/v2/search-report/product/search-texts
createProductOrder()POST /api/v2/search-report/product/orders

The CSV types are SEARCH_QUERIES_PREMIUM_REPORT_GROUP, SEARCH_QUERIES_PREMIUM_REPORT_PRODUCT and SEARCH_QUERIES_PREMIUM_REPORT_TEXT, created with createNmReportDownload().

This is data freshness, not a request-rate limit or a guarantee that every request returns new data. It is separate from the removal of five percentile columns in the TEXT CSV; other report schemas and request limits are unchanged by this cadence announcement.

Sources: Search Queries, CSV reports.

TEXT CSV columns from 2026-10-12 ​

Only SEARCH_QUERIES_PREMIUM_REPORT_TEXT stops emitting these five columns: OpenCardPercentile, AddToCartPercentile, OpenToCartPercentile, OrdersPercentile, CartToOrderPercentile.

OpenCard, AddToCart, OpenToCart, Orders, CartToOrder, their past-period metrics and all other columns remain in their existing scope. GROUP and PRODUCT CSV exports and JSON API percentile fields are unaffected by this column removal.

The SDK returns the ZIP as ArrayBuffer; it has no CSV row parser, mandatory-column checks or public CSV row interfaces. SearchReportTextReq describes the generation request, not a CSV row. In your CSV consumer, map columns by header name, accept all five missing columns, and treat historical percentile values as optional. Do not replace missing percentiles with zero or use their old numeric column positions: removing them shifts later values.

For example, after extracting and parsing a row in your application, read the retained order count by name:

typescript
// A synthetic row after 2026-10-12; no percentile columns are required.
const row: Record<string, string | undefined> = {
  Text: 'synthetic query', OpenCard: '20', AddToCart: '10',
  OpenToCart: '50', Orders: '7', CartToOrder: '70',
};
const orders = Number(row['Orders']); // 7

Source: CSV download contract.

Table of Contents ​

What are CSV Reports ​

CSV reports are long-term analytics exports generated asynchronously by the Wildberries Seller Analytics API. Unlike the JSON endpoints that return paginated data for short periods, CSV reports:

  • Cover periods up to 1 year of historical data
  • Are generated asynchronously -- you create a task, then poll until it completes
  • Are delivered as ZIP archives containing one or more CSV files
  • Require a Jam subscription (see Jam Subscription Detection)
  • Support filtering by article IDs, brands, subjects, and tags
  • Are stored on Wildberries servers for 48 hours after generation

CSV reports are ideal for feeding BI dashboards, performing offline analysis, or building automated data pipelines that aggregate weeks or months of marketplace data.

Prerequisites ​

  • SDK installed and configured with a valid API key
  • Jam subscription active on your seller account
  • API key with Analytics permissions
typescript
import { WildberriesSDK } from 'daytona-wildberries-typescript-sdk';
import { randomUUID } from 'crypto';

const sdk = new WildberriesSDK({ apiKey: process.env.WB_API_KEY! });

SDK Methods ​

The CSV report workflow uses four methods on sdk.analytics:

StepMethodPurpose
1. CreatecreateNmReportDownload(data)Submit a report generation task
2. PollgetNmReportDownloads(options?)Check generation status
3a. DownloadgetDownloadsFile(downloadId)Download the ready ZIP archive
3b. RetrycreateDownloadsRetry(data)Re-queue a FAILED report

All four methods share the same rate limit: 3 requests per minute with a 20-second interval.

createNmReportDownload ​

Creates a report generation task. You provide a seller-generated UUID, a report type, a date range, and optional filters.

typescript
const reportId = randomUUID();

const result = await sdk.analytics.createNmReportDownload({
  id: reportId,
  reportType: 'DETAIL_HISTORY_REPORT',
  userReportName: 'Q1 Sales Funnel',
  params: {
    nmIDs: [123456789, 987654321],
    startDate: '2026-01-01',
    endDate: '2026-03-31',
    timezone: 'Europe/Moscow',
    aggregationLevel: 'day',
  },
});

console.log(result.data); // "Report generation started"

Parameters:

  • id (string, required) -- UUID generated by the seller. You will use this ID to poll status and download the file.
  • reportType (string, required) -- One of the report types listed below.
  • userReportName (string, optional) -- Human-readable name for the report.
  • params (object, required) -- Filters and period configuration. Fields vary by report type.

getNmReportDownloads ​

Returns a list of reports with their current generation status. Optionally filter by specific report IDs.

typescript
// Get all reports
const allReports = await sdk.analytics.getNmReportDownloads();

// Filter by specific report IDs
const filtered = await sdk.analytics.getNmReportDownloads({
  'filter[downloadIds]': [reportId],
});

for (const report of filtered.data) {
  console.log(`${report.id}: ${report.status} (${report.name})`);
}

Response fields per report:

FieldTypeDescription
idstringReport UUID
createdAtstringTimestamp when generation completed
statusstringWAITING, PROCESSING, SUCCESS, RETRY, or FAILED
namestringReport name
sizenumberFile size in bytes
startDatestringPeriod start date
endDatestringPeriod end date

getDownloadsFile ​

Downloads a completed report as a ZIP archive. Returns an ArrayBuffer that you can write to disk or process in memory.

typescript
const zipBuffer = await sdk.analytics.getDownloadsFile(reportId);

// Write to disk
import { writeFileSync } from 'fs';
writeFileSync('report.zip', Buffer.from(zipBuffer));

createDownloadsRetry ​

Re-queues a report that ended with FAILED status. Pass the original report ID.

typescript
const retryResult = await sdk.analytics.createDownloadsRetry({
  downloadId: reportId,
});

console.log(retryResult.data); // "Report retry started"

Report Types ​

Each report type maps to a specific request interface in the SDK:

Report TypeSDK TypeDescription
DETAIL_HISTORY_REPORTSalesFunnelProductReqSales funnel data by individual products, grouped by day/week/month
GROUPED_HISTORY_REPORTSalesFunnelGroupReqSales funnel data grouped by subjects, brands, or tags
SEARCH_QUERIES_PREMIUM_REPORT_GROUPSearchReportGroupReqSearch query analytics grouped by subjects, brands, or tags
SEARCH_QUERIES_PREMIUM_REPORT_PRODUCTSearchReportProductReqSearch query analytics by individual products
SEARCH_QUERIES_PREMIUM_REPORT_TEXTSearchReportTextReqSearch query analytics by search text
STOCK_HISTORY_REPORT_CSVStocksReportReqHistorical stock levels

Sales funnel report types ​

DETAIL_HISTORY_REPORT and GROUPED_HISTORY_REPORT export sales funnel metrics (views, cart additions, orders, conversions) over time. These support the aggregationLevel parameter:

  • day -- one row per day (default)
  • week -- one row per week
  • month -- one row per month

Search query report types ​

The three SEARCH_QUERIES_PREMIUM_REPORT_* types export search query performance data. These require Jam subscription and accept includeSubstitutedSKUs and includeSearchTexts filter flags. At least one of these two flags must be true.

Stock history report type ​

STOCK_HISTORY_REPORT_CSV exports historical inventory levels using CommonReportFilters, which includes stock type filtering and availability filters.

The Create-Poll-Download Workflow ​

CSV report generation is asynchronous. The typical workflow has three steps:

1. CREATE  -->  Submit report task with UUID
2. POLL    -->  Check status every 30-60 seconds
3. DOWNLOAD -->  Fetch ZIP when status = SUCCESS

Here is a complete example:

typescript
import { WildberriesSDK } from 'daytona-wildberries-typescript-sdk';
import { randomUUID } from 'crypto';
import { writeFileSync } from 'fs';

const sdk = new WildberriesSDK({ apiKey: process.env.WB_API_KEY! });

async function generateAndDownloadReport(): Promise<void> {
  const reportId = randomUUID();

  // Step 1: Create the report task
  await sdk.analytics.createNmReportDownload({
    id: reportId,
    reportType: 'DETAIL_HISTORY_REPORT',
    userReportName: 'Monthly Sales Export',
    params: {
      startDate: '2026-01-01',
      endDate: '2026-01-31',
      timezone: 'Europe/Moscow',
      aggregationLevel: 'day',
    },
  });

  console.log(`Report ${reportId} submitted. Polling for completion...`);

  // Step 2: Poll until ready
  const status = await pollUntilReady(reportId);

  if (status === 'FAILED') {
    console.error('Report generation failed. Retrying...');
    await sdk.analytics.createDownloadsRetry({ downloadId: reportId });
    // Poll again after retry
    await pollUntilReady(reportId);
  }

  // Step 3: Download the ZIP
  const zipBuffer = await sdk.analytics.getDownloadsFile(reportId);
  const fileName = `report-${reportId}.zip`;
  writeFileSync(fileName, Buffer.from(zipBuffer));
  console.log(`Report saved to ${fileName}`);
}

async function pollUntilReady(
  reportId: string,
  maxAttempts = 30,
  intervalMs = 30_000
): Promise<string> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const response = await sdk.analytics.getNmReportDownloads({
      'filter[downloadIds]': [reportId],
    });

    const report = response.data.find((r) => r.id === reportId);

    if (!report) {
      throw new Error(`Report ${reportId} not found in downloads list`);
    }

    console.log(`  Attempt ${attempt}: status = ${report.status}`);

    if (report.status === 'SUCCESS') {
      return 'SUCCESS';
    }

    if (report.status === 'FAILED') {
      return 'FAILED';
    }

    // WAITING, PROCESSING, or RETRY -- keep polling
    if (attempt < maxAttempts) {
      await new Promise((resolve) => setTimeout(resolve, intervalMs));
    }
  }

  throw new Error(`Report ${reportId} did not complete within ${maxAttempts} attempts`);
}

Polling for Report Status ​

The report status progresses through these states:

WAITING  -->  PROCESSING  -->  SUCCESS
                           -->  FAILED  -->  (retry)  -->  RETRY  -->  SUCCESS
StatusMeaningAction
WAITINGQueued for processingContinue polling
PROCESSINGCurrently being generatedContinue polling
SUCCESSReady for downloadCall getDownloadsFile()
RETRYRe-queued after a retry requestContinue polling
FAILEDGeneration failedCall createDownloadsRetry()

Polling interval recommendations ​

  • Minimum interval: 20 seconds (matches rate limit interval)
  • Recommended interval: 30--60 seconds for most reports
  • Large reports (full year, all products): 60--120 seconds
  • Maximum attempts: 30--60 depending on report size

Rate Limit Awareness

Every poll call counts toward the shared rate limit of 3 requests per minute. With a 30-second polling interval, you consume 2 requests per minute, leaving headroom for other analytics calls.

Downloading the Report File ​

getDownloadsFile() returns an ArrayBuffer containing the ZIP archive. The ZIP contains one or more CSV files with the report data.

Save to disk ​

typescript
import { writeFileSync } from 'fs';

const zipBuffer = await sdk.analytics.getDownloadsFile(reportId);
writeFileSync(`analytics-${reportId}.zip`, Buffer.from(zipBuffer));

Extract and process in memory ​

typescript
import { Readable } from 'stream';
import unzipper from 'unzipper'; // npm install unzipper

const zipBuffer = await sdk.analytics.getDownloadsFile(reportId);
const stream = Readable.from(Buffer.from(zipBuffer));

const directory = await unzipper.Open.buffer(Buffer.from(zipBuffer));

for (const file of directory.files) {
  const content = await file.buffer();
  const csvText = content.toString('utf-8');
  console.log(`File: ${file.path}, rows: ${csvText.split('\n').length}`);
  // Parse CSV rows for your BI pipeline
}

Retrying Failed Reports ​

When a report ends with FAILED status, use createDownloadsRetry() to re-queue it. The retry uses the same report ID and parameters as the original request.

typescript
import {
  WBAPIError,
  RateLimitError,
} from 'daytona-wildberries-typescript-sdk';

async function downloadWithRetry(
  sdk: WildberriesSDK,
  reportId: string,
  maxRetries = 2
): Promise<ArrayBuffer> {
  for (let retry = 0; retry <= maxRetries; retry++) {
    const status = await pollUntilReady(reportId);

    if (status === 'SUCCESS') {
      return sdk.analytics.getDownloadsFile(reportId);
    }

    if (status === 'FAILED' && retry < maxRetries) {
      console.warn(`Report failed. Retry ${retry + 1} of ${maxRetries}...`);
      await sdk.analytics.createDownloadsRetry({ downloadId: reportId });
      // Wait before polling again
      await new Promise((resolve) => setTimeout(resolve, 30_000));
    }
  }

  throw new Error(`Report ${reportId} failed after ${maxRetries} retries`);
}

Practical Scenarios ​

Export 3-month sales history for a BI dashboard ​

typescript
import { randomUUID } from 'crypto';
import { writeFileSync } from 'fs';

async function exportQuarterlyData(sdk: WildberriesSDK): Promise<void> {
  const reportId = randomUUID();

  await sdk.analytics.createNmReportDownload({
    id: reportId,
    reportType: 'DETAIL_HISTORY_REPORT',
    userReportName: 'Q1 2026 BI Export',
    params: {
      startDate: '2026-01-01',
      endDate: '2026-03-31',
      timezone: 'Europe/Moscow',
      aggregationLevel: 'day',
      // No nmIDs filter = all products
    },
  });

  const status = await pollUntilReady(reportId);

  if (status === 'SUCCESS') {
    const zip = await sdk.analytics.getDownloadsFile(reportId);
    writeFileSync('q1-2026-sales.zip', Buffer.from(zip));
    console.log('Quarterly export complete');
  }
}

Automated daily stock report generation ​

typescript
async function dailyStockExport(sdk: WildberriesSDK): Promise<void> {
  const reportId = randomUUID();
  const today = new Date();
  const yesterday = new Date(today);
  yesterday.setDate(yesterday.getDate() - 1);

  const fmt = (d: Date) => d.toISOString().slice(0, 10);

  await sdk.analytics.createNmReportDownload({
    id: reportId,
    reportType: 'STOCK_HISTORY_REPORT_CSV',
    userReportName: `Stock Report ${fmt(yesterday)}`,
    params: {
      currentPeriod: {
        start: fmt(yesterday),
        end: fmt(today),
      },
      stockType: '',        // All warehouse types
      skipDeletedNm: true,
      availabilityFilters: [],
      orderBy: { field: 'nmId', mode: 'asc' },
    },
  });

  const status = await pollUntilReady(reportId, 20, 30_000);

  if (status === 'SUCCESS') {
    const zip = await sdk.analytics.getDownloadsFile(reportId);
    writeFileSync(`stock-${fmt(yesterday)}.zip`, Buffer.from(zip));
    console.log(`Stock report for ${fmt(yesterday)} saved`);
  } else {
    console.error('Stock report generation failed');
  }
}

Search query report for specific brands ​

typescript
async function brandSearchReport(
  sdk: WildberriesSDK,
  brandNames: string[]
): Promise<void> {
  const reportId = randomUUID();

  await sdk.analytics.createNmReportDownload({
    id: reportId,
    reportType: 'SEARCH_QUERIES_PREMIUM_REPORT_GROUP',
    userReportName: `Brand Search Report`,
    params: {
      brandNames,
      startDate: '2026-03-01',
      endDate: '2026-03-29',
      topOrderBy: 'orders',
      orderBy: { field: 'orders', mode: 'desc' },
      includeSubstitutedSKUs: true,
      includeSearchTexts: true,
      limit: 50,
    },
  });

  const status = await pollUntilReady(reportId);

  if (status === 'SUCCESS') {
    const zip = await sdk.analytics.getDownloadsFile(reportId);
    writeFileSync(`brand-search-${reportId}.zip`, Buffer.from(zip));
  }
}

Important Notes ​

File storage duration ​

Generated reports are stored on Wildberries servers for 48 hours. After that, the file is deleted and you must create a new report task. Download promptly or build automation that retrieves files soon after generation completes.

UUID generation ​

The id field in createNmReportDownload must be a valid UUID generated by the seller. Use crypto.randomUUID() (Node.js 19+) or the uuid package. Each report task must have a unique ID -- reusing an ID will conflict with an existing report.

Daily report limit ​

There is a limit of 20 reports per day per seller account. Plan your report generation accordingly. If you need multiple report types, batch them efficiently.

Aggregation levels ​

Sales funnel reports (DETAIL_HISTORY_REPORT, GROUPED_HISTORY_REPORT) support three aggregation levels:

  • day -- one data point per day (default, highest granularity)
  • week -- one data point per week
  • month -- one data point per month

Choose the aggregation level based on your analysis needs. Daily granularity over a full year produces large files.

Jam subscription requirement ​

CSV analytics reports are only available to sellers with an active Jam subscription. Without Jam, report creation requests will fail. See the Jam Subscription Detection guide to programmatically check your subscription status before attempting to generate reports.

Error Handling ​

typescript
import {
  WildberriesSDK,
  AuthenticationError,
  RateLimitError,
  ValidationError,
  NetworkError,
  WBAPIError,
} from 'daytona-wildberries-typescript-sdk';

async function safeReportGeneration(sdk: WildberriesSDK): Promise<void> {
  try {
    const reportId = randomUUID();

    await sdk.analytics.createNmReportDownload({
      id: reportId,
      reportType: 'DETAIL_HISTORY_REPORT',
      params: {
        startDate: '2026-01-01',
        endDate: '2026-03-31',
        aggregationLevel: 'day',
      },
    });
  } catch (error) {
    if (error instanceof AuthenticationError) {
      console.error('Invalid API key or insufficient permissions');
    } else if (error instanceof RateLimitError) {
      console.error(`Rate limited. Retry after ${error.retryAfter}ms`);
    } else if (error instanceof ValidationError) {
      // Invalid report type, bad date range, missing required fields
      console.error('Validation error:', error.message);
    } else if (error instanceof NetworkError) {
      console.error('Network connectivity issue:', error.message);
    } else if (error instanceof WBAPIError) {
      // Jam subscription required, daily limit exceeded, etc.
      console.error(`API error ${error.statusCode}: ${error.message}`);
    }
    throw error;
  }
}

Made with ❤️ for the Wildberries developer community