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
- Prerequisites
- SDK Methods
- Report Types
- The Create-Poll-Download Workflow
- Polling for Report Status
- Downloading the Report File
- Retrying Failed Reports
- Practical Scenarios
- Important Notes
- Error Handling
- Related Resources
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
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:
| Step | Method | Purpose |
|---|---|---|
| 1. Create | createNmReportDownload(data) | Submit a report generation task |
| 2. Poll | getNmReportDownloads(options?) | Check generation status |
| 3a. Download | getDownloadsFile(downloadId) | Download the ready ZIP archive |
| 3b. Retry | createDownloadsRetry(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.
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.
// 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:
| Field | Type | Description |
|---|---|---|
id | string | Report UUID |
createdAt | string | Timestamp when generation completed |
status | string | WAITING, PROCESSING, SUCCESS, RETRY, or FAILED |
name | string | Report name |
size | number | File size in bytes |
startDate | string | Period start date |
endDate | string | Period end date |
getDownloadsFile
Downloads a completed report as a ZIP archive. Returns an ArrayBuffer that you can write to disk or process in memory.
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.
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 Type | SDK Type | Description |
|---|---|---|
DETAIL_HISTORY_REPORT | SalesFunnelProductReq | Sales funnel data by individual products, grouped by day/week/month |
GROUPED_HISTORY_REPORT | SalesFunnelGroupReq | Sales funnel data grouped by subjects, brands, or tags |
SEARCH_QUERIES_PREMIUM_REPORT_GROUP | SearchReportGroupReq | Search query analytics grouped by subjects, brands, or tags |
SEARCH_QUERIES_PREMIUM_REPORT_PRODUCT | SearchReportProductReq | Search query analytics by individual products |
SEARCH_QUERIES_PREMIUM_REPORT_TEXT | SearchReportTextReq | Search query analytics by search text |
STOCK_HISTORY_REPORT_CSV | StocksReportReq | Historical 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 weekmonth-- 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 = SUCCESSHere is a complete example:
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| Status | Meaning | Action |
|---|---|---|
WAITING | Queued for processing | Continue polling |
PROCESSING | Currently being generated | Continue polling |
SUCCESS | Ready for download | Call getDownloadsFile() |
RETRY | Re-queued after a retry request | Continue polling |
FAILED | Generation failed | Call 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
import { writeFileSync } from 'fs';
const zipBuffer = await sdk.analytics.getDownloadsFile(reportId);
writeFileSync(`analytics-${reportId}.zip`, Buffer.from(zipBuffer));Extract and process in memory
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.
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
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
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
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 weekmonth-- 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
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;
}
}Related Resources
- Analytics Module Reference -- Full reference for all analytics methods
- Jam Subscription Detection -- Detect your Jam tier before generating CSV reports
- Sales Funnel Analytics -- Real-time sales funnel data via JSON endpoints
- Search Queries Analytics -- Real-time search query data via JSON endpoints
- Best Practices -- General SDK usage patterns and production tips