Search Queries Analytics
This guide covers the Search Queries report in the Wildberries Analytics module. You will learn how to identify which search queries bring customers to your products, track search positions over time, and use SDK methods to build data-driven SEO optimization workflows.
Target Audience: E-commerce analysts, SEO specialists, and developers building search analytics dashboards for Wildberries sellers
Prerequisites: SDK installed and configured, valid API key with Analytics permissions, active Jam subscription for search-text endpoints
Estimated Reading Time: 25 minutes
Table of Contents
- What is Search Query Analytics
- SDK Methods Overview
- Main Report Page
- Group Statistics
- Detailed Queries Within a Group
- Search Texts for a Specific Product
- Orders and Positions by Search Query
- Jam Subscription Impact
- Filtering Options
- Practical Scenarios
- Rate Limits
- Error Handling
- Related Resources
What is Search Query Analytics
Search Query Analytics provides visibility into how customers find your products on Wildberries through search. The data answers three fundamental questions:
- Which search queries drive traffic to my products? Understand the exact keywords customers use to discover your listings.
- Where do my products rank for specific queries? Track average search position across different queries and time periods.
- How do search queries convert? Measure clicks, add-to-cart actions, and orders generated by each query.
This information is essential for:
- SEO optimization -- identify high-traffic queries where your product ranks poorly and optimize card content accordingly.
- Advertising decisions -- find organic queries that already convert well and allocate advertising budget to boost them further.
- Competitive analysis -- compare how your brand performs against competitors for shared search queries.
- Content strategy -- discover which product attributes and keywords customers actually search for.
Report Structure
The Search Queries report is hierarchical. At the top level, products are organized into groups by subject, brand, and tag. Each group aggregates metrics across all products matching that grouping. You can then drill down into individual products within a group and see the specific search texts associated with each product.
Report Overview (main page)
├── Group 1 (by subject + brand + tag)
│ ├── Product A
│ │ ├── Search text: "кроссовки мужские nike"
│ │ ├── Search text: "мужская обувь для бега"
│ │ └── ...
│ ├── Product B
│ └── ...
├── Group 2
│ └── ...
└── Summary metrics (seller rating, positions, visibility)SDK Methods Overview
The Analytics module provides five methods for Search Queries analysis. Each corresponds to a different level of the report hierarchy:
| Method | Purpose | Level |
|---|---|---|
createSearchReportReport() | Main report page with summary and grouped data | Top-level overview |
createTableGroup() | Paginated group statistics (clicks, cart, orders) | Group-level |
createTableDetail() | Individual products within a specific group | Product-level |
createProductSearchText() | Top search texts for a specific product | Search-text level |
createProductOrder() | Orders and positions by search query over time | Query-level detail |
All five methods are POST requests to the seller-analytics-api.wildberries.ru domain and share the same rate limit pool.
Main Report Page
sdk.analytics.createSearchReportReport(data) returns the top-level report containing summary statistics (seller rating, advertised products count, position distribution) and the first page of grouped results.
Request
import { WildberriesSDK } from 'daytona-wildberries-typescript-sdk';
const sdk = new WildberriesSDK({ apiKey: process.env.WB_API_KEY! });
const report = await sdk.analytics.createSearchReportReport({
currentPeriod: {
start: '2025-03-01',
end: '2025-03-28',
},
pastPeriod: {
start: '2025-02-01',
end: '2025-02-28',
},
positionCluster: 'all',
orderBy: {
field: 'openCard',
mode: 'desc',
},
includeSearchTexts: true,
includeSubstitutedSKUs: false,
limit: 10,
offset: 0,
});Response Structure
The response contains three summary sections and a paginated list of groups:
const { data } = report;
// Seller-level summary
console.log('Seller rating:', data.commonInfo.supplierRating.current);
console.log('Advertised products:', data.commonInfo.advertisedProducts.current);
console.log('Total products:', data.commonInfo.totalProducts);
// Position distribution across all products
const clusters = data.positionInfo.clusters;
console.log('Products in top 100:', clusters.firstHundred.current);
console.log('Products in 101-200:', clusters.secondHundred.current);
console.log('Products below 200:', clusters.below.current);
// Average position chart over time
for (const point of data.positionInfo.chart) {
console.log(`${point.dt}: avg=${point.average}, median=${point.median}`);
}
// Visibility trend
for (const point of data.visibilityInfo.chart) {
console.log(`${point.dt}: visibility=${point.value}`);
}
// Grouped results (first page)
if (data.groups) {
for (const group of data.groups) {
console.log(`Group: ${group.subjectName} / ${group.brandName}`);
console.log(` Avg position: ${group.metrics.avgPosition.current}`);
console.log(` Card opens: ${group.metrics.openCard.current}`);
console.log(` Add to cart: ${group.metrics.addToCart.current}`);
console.log(` Orders: ${group.metrics.orders.current}`);
}
}Key Parameters
| Parameter | Type | Description |
|---|---|---|
currentPeriod | Period | Date range for the report (max 365 days from today) |
pastPeriod | PastPeriod | Optional comparison period (same or shorter duration) |
positionCluster | 'all' | 'firstHundred' | 'secondHundred' | 'below' | Filter by average search position range |
orderBy | OrderBy | Sort field and direction |
limit / offset | number | Pagination for groups |
nmIds | number[] | Optional: filter by WB article IDs |
brandNames | string[] | Optional: filter by brand names |
subjectIds | number[] | Optional: filter by subject (category) IDs |
tagIds | number[] | Optional: filter by tag IDs |
Group Statistics
sdk.analytics.createTableGroup(data) retrieves additional pages of group-level statistics. Use this when the main report returns more groups than fit in a single response.
Pagination Prerequisite
Group pagination requires at least one active filter (brandNames, subjectIds, or tagIds). Without a filter, the endpoint returns the full result set in the main report.
const groups = await sdk.analytics.createTableGroup({
currentPeriod: {
start: '2025-03-01',
end: '2025-03-28',
},
positionCluster: 'all',
orderBy: {
field: 'orders',
mode: 'desc',
},
brandNames: ['MyBrand'],
includeSearchTexts: true,
includeSubstitutedSKUs: false,
limit: 20,
offset: 0,
});
for (const group of groups.data.groups) {
const m = group.metrics;
console.log(`${group.subjectName} | ${group.brandName}`);
console.log(` Position: ${m.avgPosition.current} (${m.avgPosition.dynamics}%)`);
console.log(` Card opens: ${m.openCard.current}`);
console.log(` Add to cart: ${m.addToCart.current} (conv: ${m.openToCart.current}%)`);
console.log(` Orders: ${m.orders.current} (conv: ${m.cartToOrder.current}%)`);
}Each group contains metrics with current values and optional dynamics percentages showing change compared to pastPeriod.
Detailed Queries Within a Group
sdk.analytics.createTableDetail(data) returns individual products within a group. Use it to drill down from a group-level summary into per-product performance.
const details = await sdk.analytics.createTableDetail({
currentPeriod: {
start: '2025-03-01',
end: '2025-03-28',
},
// Filter to a specific group
subjectId: 306,
brandName: 'MyBrand',
positionCluster: 'firstHundred',
orderBy: {
field: 'orders',
mode: 'desc',
},
includeSearchTexts: true,
includeSubstitutedSKUs: false,
limit: 50,
offset: 0,
});
for (const product of details.data.products) {
console.log(`Product: ${product.name} (nmId: ${product.nmId})`);
console.log(` Brand: ${product.brandName}`);
console.log(` Vendor code: ${product.vendorCode}`);
console.log(` Is advertised: ${product.isAdvertised}`);
console.log(` Rating: ${product.rating}`);
if (product.price) {
console.log(` Price range: ${product.price.min} - ${product.price.max}`);
}
}Product-Level Fields
Each TableProductItem in the response includes:
nmId-- WB article numbername,vendorCode,brandName,subjectName-- product identificationisAdvertised-- whether the product is currently in search promotionisSubstitutedSKU-- whether search used a substitute articlerating,feedbackRating-- product and review ratingsprice.min,price.max-- price range after seller discountsmainPhoto-- URL to the primary product image- Metrics (
avgPosition,openCard,addToCart,openToCart,orders,cartToOrder,visibility) withcurrentanddynamicsvalues
Search Texts for a Specific Product
sdk.analytics.createProductSearchText(data) returns the top search queries that led customers to a specific product. This is the most granular level of the report and is directly affected by your Jam subscription tier.
import { WildberriesSDK } from 'daytona-wildberries-typescript-sdk';
const sdk = new WildberriesSDK({ apiKey: process.env.WB_API_KEY! });
const searchTexts = await sdk.analytics.createProductSearchText({
currentPeriod: {
start: '2025-03-01',
end: '2025-03-28',
},
nmIds: [123456789],
topOrderBy: 'openCard',
orderBy: {
field: 'openCard',
mode: 'desc',
},
includeSearchTexts: true,
includeSubstitutedSKUs: false,
limit: 30, // 30 for Standard tier, 50 for Advanced tier
});
console.log('Top search texts:', searchTexts.data.items);The topOrderBy Parameter
This parameter determines the ranking criterion for which search texts appear in the top results:
| Value | Meaning |
|---|---|
'openCard' | Queries that generated the most card opens |
'addToCart' | Queries that led to the most add-to-cart actions |
'openToCart' | Queries with the highest card-to-cart conversion rate |
'orders' | Queries that resulted in the most orders |
'cartToOrder' | Queries with the highest cart-to-order conversion rate |
Limit and Jam Subscription
The limit field controls how many search texts are returned. The maximum value depends on your Jam subscription tier:
| Tier | Maximum limit |
|---|---|
| Standard | 30 |
| Advanced | 50 |
| None | Endpoint unavailable (400 error) |
See Jam Subscription Impact below for how to detect your tier automatically.
Orders and Positions by Search Query
sdk.analytics.createProductOrder(data) shows how a product performed for specific search queries over time -- the number of orders and the average search position for each query on each date.
const orders = await sdk.analytics.createProductOrder({
period: {
start: '2025-03-01',
end: '2025-03-28',
},
nmId: 123456789,
searchTexts: [
'кроссовки мужские',
'мужская обувь для бега',
'nike кроссовки',
],
});
// Total metrics across all dates
for (const total of orders.data.total) {
console.log(`${total.dt}: position=${total.avgPosition}, orders=${total.orders}`);
}
// Per-query breakdown
for (const item of orders.data.items) {
console.log(`Query: "${item.text}" (frequency: ${item.frequency})`);
for (const day of item.dateItems) {
console.log(` ${day.dt}: position=${day.avgPosition}, orders=${day.orders}`);
}
}Key Differences from Other Methods
- Uses a
periodfield (notcurrentPeriod) withstartandenddates. - Takes a single
nmId(not an array). - Requires you to specify the exact
searchTextsto analyze (up to 100 for Advanced tier). - Returns daily time-series data with position and order counts per query.
This method is ideal for tracking how a product's ranking evolves for specific keywords over time.
Jam Subscription Impact
The Wildberries Jam subscription directly affects the createProductSearchText() method. Without a Jam subscription, the endpoint returns a 400 error. With a subscription, the maximum limit value depends on your tier.
Detecting Your Tier
Use sdk.general.getJamSubscriptionStatus() to determine your tier programmatically:
import { WildberriesSDK } from 'daytona-wildberries-typescript-sdk';
const sdk = new WildberriesSDK({ apiKey: process.env.WB_API_KEY! });
async function getSearchTextsWithCorrectLimit(nmIds: number[]) {
// Step 1: Detect Jam tier
const jamStatus = await sdk.general.getJamSubscriptionStatus({
nmIds,
});
console.log(`Jam tier: ${jamStatus.tier}`);
// Step 2: Handle "none" tier
if (jamStatus.tier === 'none') {
console.warn(
'Jam subscription required for search-text analytics. ' +
'Falling back to group-level report.'
);
// Use createSearchReportReport() instead, which does not require Jam
return null;
}
// Step 3: Set limit based on tier
const limit = jamStatus.tier === 'advanced' ? 50 : 30;
// Step 4: Fetch search texts
const searchTexts = await sdk.analytics.createProductSearchText({
currentPeriod: {
start: '2025-03-01',
end: '2025-03-28',
},
nmIds,
topOrderBy: 'openCard',
orderBy: { field: 'openCard', mode: 'desc' },
includeSearchTexts: true,
includeSubstitutedSKUs: false,
limit,
});
return searchTexts.data.items;
}Cache the Jam Tier
Your subscription tier does not change frequently. Detect it once at application startup and reuse the result for all subsequent calls. See the Jam Subscription Detection guide for caching patterns.
Methods That Do Not Require Jam
The following methods work regardless of Jam subscription status:
createSearchReportReport()-- main report pagecreateTableGroup()-- group paginationcreateTableDetail()-- product-level details within a groupcreateProductOrder()-- orders and positions (but you need search texts to pass as input, typically obtained fromcreateProductSearchText())
Filtering Options
All search report methods support a consistent set of filters to narrow down results.
Common Filters
| Filter | Type | Available In | Description |
|---|---|---|---|
nmIds | number[] | All methods | Filter by WB article IDs |
brandNames | string[] | Main report, groups | Filter by brand names |
subjectIds | number[] | Main report, groups | Filter by subject (category) IDs |
tagIds | number[] | Main report, groups | Filter by seller-defined tag IDs |
positionCluster | string | Main report, groups, details | Filter by position range |
includeSearchTexts | boolean | All except createProductOrder | Include search text data |
includeSubstitutedSKUs | boolean | All except createProductOrder | Include substituted article data |
Boolean Constraint
includeSubstitutedSKUs and includeSearchTexts cannot both be false. At least one must be true.
Sorting Options
The orderBy field supports different sort fields depending on the method:
Main report and details (OrderBy): avgPosition, openCard, addToCart, openToCart, orders, cartToOrder, visibility, minPrice, maxPrice
Groups and search texts (OrderByGrTe): avgPosition, openCard, addToCart, openToCart, orders, cartToOrder, visibility
Position Clusters
The positionCluster parameter filters results by average search position range:
| Value | Position Range |
|---|---|
'all' | All positions |
'firstHundred' | 1 -- 100 |
'secondHundred' | 101 -- 200 |
'below' | 201 and lower |
Example: Filtered Report
// Get report for a specific brand, sorted by orders, top-100 positions only
const report = await sdk.analytics.createSearchReportReport({
currentPeriod: { start: '2025-03-01', end: '2025-03-28' },
brandNames: ['MyBrand'],
subjectIds: [306], // e.g., "Sneakers" category
positionCluster: 'firstHundred',
orderBy: { field: 'orders', mode: 'desc' },
includeSearchTexts: true,
includeSubstitutedSKUs: false,
limit: 20,
offset: 0,
});Practical Scenarios
Scenario 1: SEO Optimization -- Find Top Search Queries for a Product
Identify which search queries bring the most traffic to a specific product, then check whether your product card content aligns with those queries.
import { WildberriesSDK } from 'daytona-wildberries-typescript-sdk';
const sdk = new WildberriesSDK({ apiKey: process.env.WB_API_KEY! });
const PRODUCT_ID = 123456789;
async function analyzeProductSEO() {
// 1. Detect Jam tier and set correct limit
const jamStatus = await sdk.general.getJamSubscriptionStatus({
nmIds: [PRODUCT_ID],
});
if (jamStatus.tier === 'none') {
console.error('Jam subscription required for search text analysis');
return;
}
const limit = jamStatus.tier === 'advanced' ? 50 : 30;
// 2. Get top search queries by card opens
const byOpens = await sdk.analytics.createProductSearchText({
currentPeriod: { start: '2025-03-01', end: '2025-03-28' },
nmIds: [PRODUCT_ID],
topOrderBy: 'openCard',
orderBy: { field: 'openCard', mode: 'desc' },
includeSearchTexts: true,
includeSubstitutedSKUs: false,
limit,
});
// 3. Get top search queries by orders (conversion-oriented)
const byOrders = await sdk.analytics.createProductSearchText({
currentPeriod: { start: '2025-03-01', end: '2025-03-28' },
nmIds: [PRODUCT_ID],
topOrderBy: 'orders',
orderBy: { field: 'orders', mode: 'desc' },
includeSearchTexts: true,
includeSubstitutedSKUs: false,
limit,
});
console.log('--- Top queries by traffic ---');
console.log(byOpens.data.items);
console.log('--- Top queries by orders ---');
console.log(byOrders.data.items);
// 4. Compare: queries with high traffic but low orders indicate
// optimization opportunities (improve card content, pricing, images)
}
analyzeProductSEO();Scenario 2: Monitor Search Position Changes
Track how your product's search position changes over time for key queries.
async function monitorPositionChanges() {
const PRODUCT_ID = 123456789;
const TARGET_QUERIES = [
'кроссовки мужские',
'спортивная обувь',
'nike кроссовки',
];
// Get position data for the last 4 weeks
const data = await sdk.analytics.createProductOrder({
period: {
start: '2025-03-01',
end: '2025-03-28',
},
nmId: PRODUCT_ID,
searchTexts: TARGET_QUERIES,
});
for (const item of data.data.items) {
console.log(`\nQuery: "${item.text}" (frequency: ${item.frequency})`);
if (item.dateItems.length < 2) {
console.log(' Not enough data points for trend analysis');
continue;
}
const first = item.dateItems[0];
const last = item.dateItems[item.dateItems.length - 1];
const positionDelta = last.avgPosition - first.avgPosition;
const trend = positionDelta < 0 ? 'IMPROVED' : positionDelta > 0 ? 'DECLINED' : 'STABLE';
console.log(` Position: ${first.avgPosition} -> ${last.avgPosition} (${trend})`);
console.log(` Total orders in period: ${item.dateItems.reduce((sum, d) => sum + d.orders, 0)}`);
}
}
monitorPositionChanges();Scenario 3: Compare Keyword Performance Across Brands
Use the group-level report to compare how different brands in the same category perform for search queries.
async function compareBrandPerformance(brandNames: string[], subjectId: number) {
const results: Array<{ brand: string; avgPosition: number; orders: number; openCard: number }> = [];
for (const brand of brandNames) {
const report = await sdk.analytics.createSearchReportReport({
currentPeriod: { start: '2025-03-01', end: '2025-03-28' },
brandNames: [brand],
subjectIds: [subjectId],
positionCluster: 'all',
orderBy: { field: 'orders', mode: 'desc' },
includeSearchTexts: true,
includeSubstitutedSKUs: false,
limit: 5,
offset: 0,
});
const groups = report.data.groups ?? [];
for (const group of groups) {
results.push({
brand: group.brandName ?? brand,
avgPosition: group.metrics.avgPosition.current,
orders: group.metrics.orders.current,
openCard: group.metrics.openCard.current,
});
}
}
// Sort by orders descending
results.sort((a, b) => b.orders - a.orders);
console.log('Brand Performance Comparison:');
console.log('---------------------------------------------');
for (const r of results) {
console.log(
`${r.brand.padEnd(20)} | ` +
`Pos: ${String(r.avgPosition).padStart(5)} | ` +
`Opens: ${String(r.openCard).padStart(6)} | ` +
`Orders: ${String(r.orders).padStart(6)}`
);
}
}
compareBrandPerformance(['BrandA', 'BrandB', 'BrandC'], 306);Scenario 4: Identify Low-Conversion Queries to Optimize
Find queries where your product gets many card opens but few orders -- these represent optimization opportunities.
async function findLowConversionQueries() {
const PRODUCT_ID = 123456789;
// Check Jam tier
const jamStatus = await sdk.general.getJamSubscriptionStatus({
nmIds: [PRODUCT_ID],
});
if (jamStatus.tier === 'none') {
console.error('Jam subscription required');
return;
}
const limit = jamStatus.tier === 'advanced' ? 50 : 30;
// Get top queries by card opens (traffic)
const trafficQueries = await sdk.analytics.createProductSearchText({
currentPeriod: { start: '2025-03-01', end: '2025-03-28' },
nmIds: [PRODUCT_ID],
topOrderBy: 'openCard',
orderBy: { field: 'openCard', mode: 'desc' },
includeSearchTexts: true,
includeSubstitutedSKUs: false,
limit,
});
// Get top queries by cart-to-order conversion (efficiency)
const conversionQueries = await sdk.analytics.createProductSearchText({
currentPeriod: { start: '2025-03-01', end: '2025-03-28' },
nmIds: [PRODUCT_ID],
topOrderBy: 'cartToOrder',
orderBy: { field: 'cartToOrder', mode: 'desc' },
includeSearchTexts: true,
includeSubstitutedSKUs: false,
limit,
});
console.log('High-traffic queries (potential optimization targets):');
console.log(trafficQueries.data.items);
console.log('\nHigh-conversion queries (already performing well):');
console.log(conversionQueries.data.items);
// Queries in trafficQueries but NOT in conversionQueries
// are high-traffic, low-conversion -- prime optimization targets.
// Consider:
// - Improving product images for relevance to the query
// - Adjusting pricing to match buyer intent
// - Updating product descriptions with query-relevant keywords
// - Adding advertising budget for queries with proven traffic
}
findLowConversionQueries();Rate Limits
All five search report methods share a common rate limit pool on the seller-analytics-api.wildberries.ru domain:
| Parameter | Value |
|---|---|
| Requests per minute | 3 |
| Interval | 20 seconds |
| Burst limit | 3 |
Shared Quota
The rate limit is shared across all search report endpoints. A call to createSearchReportReport() followed immediately by createTableGroup() counts as 2 of your 3 requests per minute. Space your calls accordingly.
Recommended Approach
// Helper to add delay between analytics calls
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function fetchFullReport() {
// 1. Main report
const main = await sdk.analytics.createSearchReportReport({ /* ... */ });
// Wait 20 seconds before next call
await delay(20_000);
// 2. Group details
const groups = await sdk.analytics.createTableGroup({ /* ... */ });
await delay(20_000);
// 3. Product details
const details = await sdk.analytics.createTableDetail({ /* ... */ });
}Error Handling
import {
WildberriesSDK,
AuthenticationError,
RateLimitError,
ValidationError,
NetworkError,
WBAPIError,
} from 'daytona-wildberries-typescript-sdk';
const sdk = new WildberriesSDK({ apiKey: process.env.WB_API_KEY! });
async function safeSearchReport() {
try {
const report = await sdk.analytics.createSearchReportReport({
currentPeriod: { start: '2025-03-01', end: '2025-03-28' },
positionCluster: 'all',
orderBy: { field: 'openCard', mode: 'desc' },
includeSearchTexts: true,
includeSubstitutedSKUs: false,
limit: 10,
offset: 0,
});
return report;
} catch (error) {
if (error instanceof AuthenticationError) {
console.error('Invalid API key or missing Analytics permission');
} else if (error instanceof RateLimitError) {
console.error(`Rate limited. Retry after ${error.retryAfter}ms`);
} else if (error instanceof ValidationError) {
// Common causes:
// - Both includeSubstitutedSKUs and includeSearchTexts are false
// - limit exceeds Jam tier maximum (for createProductSearchText)
// - Invalid date range (start > end, or > 365 days from today)
console.error('Invalid request parameters:', error.message);
} else if (error instanceof NetworkError) {
console.error('Network error:', error.message);
} else if (error instanceof WBAPIError) {
console.error(`API error ${error.statusCode}: ${error.message}`);
}
throw error;
}
}Related Resources
- Analytics Module Reference -- Full reference for all Analytics module methods
- Jam Subscription Detection -- Detect your Jam tier and set correct limits
- Sales Funnel Analytics Best Practices -- Organic and advertising funnel analysis
- Best Practices -- General error handling and production patterns
- Configuration Guide -- SDK configuration and timeout settings