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.

Table of Contents

What are CSV Reports

CSV reports are long-term analytics exports generated asynchronously by the Wildberries Seller Analytics API. Unlike the real-time 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