Skip to content

Advertising Campaign Best Practices

Complete guide to managing Wildberries advertising campaigns using the SDK — from campaign creation to performance optimization.

Target Audience: E-commerce developers building advertising management tools for Wildberries marketplace Prerequisites: Basic SDK setup, familiarity with TypeScript and async/await Estimated Reading Time: 30 minutes


Table of Contents

  1. Campaign Architecture
  2. Campaign Lifecycle Best Practices
  3. Budget Management
  4. Bid Optimization
  5. Keyword & Phrase Management
  6. Campaign Statistics & Monitoring
  7. WB Marketplace Promotions
  8. Rate Limit Strategy
  9. Error Handling & Troubleshooting
  10. Complete Workflow Example

Campaign Architecture

Campaign Types

TypeDescriptionStatus
4In catalogDeprecated
5In product cardDeprecated
6In searchDeprecated
7In recommendationsDeprecated
8Unified bidDeprecated
9Unified or manual bidCurrent

Types 4–8 Are Legacy

All new campaigns should use type 9. Types 4–8 are deprecated. Use getAdvertsV2() to list campaigns of any type — the legacy getAuctionAdverts() (type 9) and createPromotionAdvert() (types 4-8) were removed in v4.0.0.

Campaign Statuses

StatusCodeDescription
Ready4Campaign created, ready to launch
Finished7Campaign ended
Cancelled8Campaign cancelled
Active9Campaign is running
Paused11Campaign on pause
Deleting-1Campaign being deleted (3–10 min)

Bid Types

Bid TypeDescriptionUse Case
manualSet bids per product and placement individuallyFine-grained control over spend
unifiedSingle bid for all products and placementsSimpler management, broader reach

Placement Types

PlacementDescription
searchAds in search results
recommendationsAds in recommendation blocks
combinedBoth search and recommendations

Status Transition Diagram

┌─────────┐     start      ┌────────┐
│  Ready  │ ──────────────> │ Active │
│   (4)   │                 │   (9)  │
└────┬────┘                 └───┬────┘
     │                          │
     │ delete                   │ pause
     ▼                          ▼
┌──────────┐             ┌─────────┐
│ Deleting │             │ Paused  │
│   (-1)   │             │  (11)   │
└──────────┘             └────┬────┘

     ┌────────────────────────┤ start
     │                        │
     │      stop              ▼
     │    ◄───────────────────┘

┌──────────┐
│ Finished │
│   (7)    │
└──────────┘

Campaign Lifecycle Best Practices

Pre-Launch Checklist

Before creating a campaign:

  1. Check account balance — ensure sufficient funds
  2. Verify products — products must be in stock and eligible for advertising
  3. Choose bid typemanual for granular control, unified for simplicity
  4. Choose placementsearch for purchase-intent traffic, recommendations for discovery

Creating a Campaign

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

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

// Step 1: Check balance before creating
const balance = await sdk.promotion.getAdvBalance();
console.log(`Available balance: ${balance.net}₽`);

if (balance.net < 500) {
  throw new Error('Insufficient balance for advertising');
}

// Step 2: Create campaign
const campaignId = await sdk.promotion.createCampaign({
  name: 'Winter Collection 2026',
  nms: [168120815, 173574852], // Product NM IDs (max 50)
  bid_type: 'manual',
  payment_type: 'cpm',
  placement_types: ['search']
});

console.log(`Campaign created: ID ${campaignId}`);

Launch Sequence

The correct sequence is: Create → Deposit Budget → Start.

typescript
const delay = (ms: number) => new Promise(r => setTimeout(r, ms));

// 1. Create campaign (returns in status 4 = Ready)
const campaignId = await sdk.promotion.createCampaign({
  name: 'Spring Sale 2026',
  nms: [168120815],
  bid_type: 'unified',
  payment_type: 'cpm',
  placement_types: ['search', 'recommendations']
});
await delay(5000);

// 2. Deposit budget (campaign must be in status 4 or 11)
await sdk.promotion.createBudgetDeposit(
  { sum: 1000, type: 1 }, // type: 0=account, 1=cabinet balance, 3=bonus
  { id: campaignId as number }
);
await delay(5000);

// 3. Start campaign (moves from status 4 → 9)
await sdk.promotion.startCampaign(campaignId as number);
console.log('Campaign launched!');

Campaign Control Methods

typescript
// Pause active campaign (status 9 → 11)
await sdk.promotion.pauseCampaign(campaignId);

// Resume paused campaign (status 11 → 9)
await sdk.promotion.startCampaign(campaignId);

// Stop campaign permanently (status 4/9/11 → 7)
await sdk.promotion.getAdvStop({ id: campaignId });

// Delete campaign (status 4 only → -1)
await sdk.promotion.getAdvDelete({ id: campaignId });

// Rename campaign (any status)
await sdk.promotion.createAdvRename({
  advertId: campaignId,
  name: 'Updated Campaign Name'
});

Status Requirements

  • Budget deposit: Campaign must be paused (status 11) or ready (status 4)
  • Delete: Only works for status 4 (Ready). Active campaigns must be stopped first.
  • Phrase management: Campaign must be active (status 9)

Listing All Campaigns

typescript
// Get all campaigns grouped by type and status
const overview = await sdk.promotion.getCampaignCount();
console.log(`Total campaigns: ${overview.all}`);

overview.adverts?.forEach(group => {
  console.log(`Type ${group.type}, Status ${group.status}: ${group.count} campaigns`);
  group.advert_list?.forEach(ad => {
    console.log(`  ID: ${ad.advertId}, Last changed: ${ad.changeTime}`);
  });
});

Getting Campaign Details

Use getAdvertsV2() to retrieve filtered campaign details. This replaces the removed getAuctionAdverts() (type 9) and createPromotionAdvert() (types 4-8).

typescript
// Active CPM campaigns
const activeDetails = await sdk.promotion.getAdvertsV2({
  statuses: '9',      // Filter: active only
  payment_type: 'cpm' // Filter: CPM campaigns
});

Budget Management

Checking Account Balance

typescript
const balance = await sdk.promotion.getAdvBalance();
console.log(`Account balance (счёт): ${balance.balance}₽`);
console.log(`Cabinet balance (кабинет): ${balance.net}₽`);
console.log(`Bonus: ${balance.bonus}₽`);

if (balance.cashbacks?.length) {
  balance.cashbacks.forEach(cb => {
    console.log(`Cashback: ${cb.percent}% (max ${cb.sum}₽)`);
  });
}

Depositing Budget

Best Practice

Always pause the campaign before depositing budget to avoid race conditions.

typescript
// Pause campaign first
await sdk.promotion.pauseCampaign(campaignId);
await delay(2000);

// Deposit 1000₽ from cabinet balance
await sdk.promotion.createBudgetDeposit(
  {
    sum: 1000,
    type: 1  // 0 = account (счёт), 1 = cabinet (баланс), 3 = bonus
  },
  { id: campaignId }
);

// Verify budget
const budget = await sdk.promotion.getAdvBudget({ id: campaignId });
console.log(`Campaign budget: cash=${budget.cash}₽, netting=${budget.netting}₽, total=${budget.total}₽`);

// Resume campaign
await sdk.promotion.startCampaign(campaignId);

Expense Tracking

typescript
const to = new Date();
const from = new Date();
from.setDate(from.getDate() - 30);

// Get expense history (УПД)
const expenses = await sdk.promotion.getAdvUpd({
  from: from.toISOString().split('T')[0],
  to: to.toISOString().split('T')[0]
});

let totalSpent = 0;
expenses.forEach(record => {
  console.log(`${record.campName}: ${record.updSum}₽`);
  totalSpent += record.updSum || 0;
});
console.log(`Total spent in 30 days: ${totalSpent}₽`);

// Get payment (top-up) history
const payments = await sdk.promotion.getAdvPayments({
  from: from.toISOString().split('T')[0],
  to: to.toISOString().split('T')[0]
});

Budget Monitoring Function

typescript
async function monitorBudget(sdk: WildberriesSDK, campaignId: number, threshold: number) {
  const budget = await sdk.promotion.getAdvBudget({ id: campaignId });

  if (budget.total < threshold) {
    console.warn(`⚠️ Campaign ${campaignId} budget low: ${budget.total}₽ (threshold: ${threshold}₽)`);

    // Auto-deposit from cabinet balance
    const balance = await sdk.promotion.getAdvBalance();
    if (balance.net >= 500) {
      await sdk.promotion.pauseCampaign(campaignId);
      await delay(2000);
      await sdk.promotion.createBudgetDeposit(
        { sum: 500, type: 1 },
        { id: campaignId }
      );
      await sdk.promotion.startCampaign(campaignId);
      console.log(`✅ Deposited 500₽ to campaign ${campaignId}`);
    } else {
      console.error(`❌ Insufficient cabinet balance: ${balance.net}₽`);
    }
  }
}

Bid Optimization

Getting Minimum Bids

Before setting bids, check the minimum required (V2 — kopecks):

typescript
const minBids = await sdk.promotion.getBidsMinV2({
  advert_id: campaignId,
  nm_ids: [168120815, 173574852],
  payment_type: 'cpm',  // 'cpm' (per 1000 impressions) or 'cpc' (per click)
  placement_types: ['search']
});

minBids.bids.forEach(bid => {
  console.log(`Product ${bid.nm_id}: min bid = ${bid.bid}₽`);
});

Updating Bids (Kopecks)

updateBids() is the single bid-update method (kopecks-based). It handles both unified and manual bid campaigns — the rubles-based updateAdvBid() and updateAuctionBid() were removed in v4.0.0.

For campaigns with bid_type: 'manual' — set per-product, per-placement bids:

typescript
await sdk.promotion.updateBids({
  bids: [{
    advert_id: campaignId,
    nm_bids: [
      {
        nm_id: 168120815,
        bid_kopecks: 30000,  // 300₽ in kopecks
        placement: 'search'
      },
      {
        nm_id: 168120815,
        bid_kopecks: 20000,  // 200₽
        placement: 'recommendations'
      },
      {
        nm_id: 173574852,
        bid_kopecks: 25000,  // 250₽
        placement: 'search'
      }
    ]
  }]
});

CPM vs CPC Strategy

StrategyWhen to UseAdvantage
CPM (cost per 1000 impressions)Brand awareness, new productsPredictable cost, maximum visibility
CPC (cost per click)Performance campaigns, proven productsPay only for engagement

Bid Optimization Strategy

  1. Start with minimum bids to test performance
  2. Monitor CTR and conversion for 3–5 days
  3. Increase bids gradually for high-performing products
  4. Reduce or pause bids for products with low ROAS

Keyword & Phrase Management

Active Campaign Required

Keyword and phrase management methods require the campaign to be in active status (9). Start the campaign before modifying keywords.

Excluded Phrases (Minus-Words)

Remove irrelevant search queries to improve ad targeting. The removed createSearchSetExcluded() / createAutoSetExcluded() methods are replaced by the normquery-based setMinusPhrases():

typescript
// Set minus-phrases for a campaign+product
await sdk.promotion.setMinusPhrases({
  advert_id: campaignId,
  nm_id: 168120815,
  norm_queries: ['cheap', 'discount', 'used', 'broken']
});

// Clear all minus-phrases (pass an empty array)
await sdk.promotion.setMinusPhrases({
  advert_id: campaignId,
  nm_id: 168120815,
  norm_queries: []
});

Fixed Phrases (Removed in v4.0.0)

The fixed-phrase management methods (createSearchSetPlu(), getSearchSetPlus()) were removed in v4.0.0 — WB dropped the underlying v1 search-phrase endpoints. There is no 1:1 replacement. For keyword-level performance, use getAdvFullstats().

Managing Products in Campaign

typescript
// Get products available to add (by subject)
const available = await sdk.promotion.getSupplierNms([subjectId]);
console.log(`Available products: ${available.length}`);

// Add/remove products
await sdk.promotion.updateCampaignProducts({
  campaigns: [{
    advert_id: campaignId,
    add_nms: [168120815, 173574852],    // Add these products
    delete_nms: [111111111]              // Remove this product
  }]
});

Keyword Refinement Workflow

Iterative Keyword Optimization

  1. Launch campaign with broad targeting
  2. After 3–5 days, check getAdvFullstats() for keyword/SKU performance
  3. Add low-performing keywords to minus-phrases via setMinusPhrases()
  4. Repeat every 1–2 weeks

Campaign Statistics & Monitoring

Full Campaign Statistics

Strict Rate Limit

getAdvFullstats() allows only 3 requests per minute with 20-second intervals. Exceeding this will result in 429 errors.

typescript
const fullStats = await sdk.promotion.getAdvFullstats({
  ids: String(campaignId),     // Comma-separated, max 100 IDs
  beginDate: '2026-01-01',
  endDate: '2026-01-31'        // Max 31 days from beginDate
});

fullStats.forEach(stat => {
  console.log(`Campaign ${stat.advertId}:`);
  console.log(`  Views: ${stat.views}`);
  console.log(`  Clicks: ${stat.clicks}`);
  console.log(`  CTR: ${stat.ctr}%`);
  console.log(`  CPC: ${stat.cpc}₽`);
  console.log(`  Orders: ${stat.orders}`);
  console.log(`  Add to cart: ${stat.atbs}`);
  console.log(`  Conversion: ${stat.cr}%`);
  console.log(`  Total spent: ${stat.sum}₽`);
});

SKU-Level Performance Drill-Down

The response has a nested structure: campaign → days[] → apps[] → nms[].

typescript
const PLATFORMS: Record<number, string> = {
  1: 'Website',
  32: 'Android',
  64: 'iOS'
};

interface SkuMetrics {
  nmId: number;
  name: string;
  clicks: number;
  views: number;
  orders: number;
  spent: number;
}

const skuMap = new Map<number, SkuMetrics>();

for (const campaign of fullStats) {
  for (const day of campaign.days) {
    for (const app of day.apps) {
      for (const nm of app.nms) {
        const existing = skuMap.get(nm.nmId) || {
          nmId: nm.nmId,
          name: nm.name,
          clicks: 0, views: 0, orders: 0, spent: 0
        };
        existing.clicks += nm.clicks;
        existing.views += nm.views;
        existing.orders += nm.orders;
        existing.spent += nm.sum;
        skuMap.set(nm.nmId, existing);
      }
    }
  }
}

for (const [nmId, metrics] of skuMap) {
  const cpa = metrics.orders > 0 ? (metrics.spent / metrics.orders).toFixed(2) : 'N/A';
  console.log(`SKU ${nmId} (${metrics.name}): ${metrics.clicks} clicks, ${metrics.orders} orders, ${metrics.spent}₽ spent, CPA=${cpa}₽`);
}

Keyword Statistics

The standalone getStatsKeywords() method was removed in v4.0.0. Use getAdvFullstats() for keyword/SKU-level performance — it returns per-day, per-platform, and per-SKU breakdowns including clicks, views, CTR, and spend.

typescript
// Campaign performance for last 7 days
const stats = await sdk.promotion.getAdvFullstats({
  ids: String(campaignId),
  beginDate: '2026-01-25',
  endDate: '2026-01-31'     // Max 31 days range
});

stats.forEach(campaign => {
  console.log(`Campaign ${campaign.advertId}: ${campaign.views} views, ${campaign.clicks} clicks, spend=${campaign.sum}₽`);
  campaign.days.forEach(day => {
    console.log(`  ${day.date}: views=${day.views}, clicks=${day.clicks}, spend=${day.sum}₽`);
  });
});

The bid-type-specific stats methods getStatWords() (manual bid) and getAutoStatWords() (unified bid) were also removed in v4.0.0. getAdvFullstats() is now the universal statistics method for all campaign types.

Rate-Limit-Safe Polling Pattern

typescript
const SAFE_DELAY = 21000; // 21 seconds between requests

async function getStatsForCampaigns(sdk: WildberriesSDK, campaignIds: number[], period: { start: string; end: string }) {
  const results = [];

  // Batch: up to 100 IDs per request
  for (let i = 0; i < campaignIds.length; i += 100) {
    const batch = campaignIds.slice(i, i + 100);

    try {
      const stats = await sdk.promotion.getAdvFullstats({
        ids: batch.join(','),
        beginDate: period.start,
        endDate: period.end
      });
      results.push(...stats);
    } catch (error) {
      if (error instanceof RateLimitError) {
        console.log(`Rate limited, waiting ${error.retryAfter}ms`);
        await delay(error.retryAfter);
        i -= 100; // Retry this batch
        continue;
      }
      throw error;
    }

    // Wait between batches
    if (i + 100 < campaignIds.length) {
      await delay(SAFE_DELAY);
    }
  }

  return results;
}

WB Marketplace Promotions

Promotions ≠ Advertising Campaigns

WB Marketplace Promotions are sales events organized by Wildberries (e.g., "Black Friday", "Flash Sales"). They are different from Advertising Campaigns that sellers create and manage.

AspectPromotionsAdvertising
CreatorWildberriesSeller
API Domaindp-calendar-api.wildberries.ruadvert-api.wildberries.ru
PurposeMarketplace-wide sales eventsPaid product advertising
CostUsually free participationPaid per click/impression

Working with Promotions Calendar

typescript
// Get available promotions for a date range
const promotions = await sdk.promotion.getCalendarPromotions({
  startDateTime: '2026-01-01T00:00:00Z',
  endDateTime: '2026-12-31T23:59:59Z',
  allPromo: true,
  limit: 100,
  offset: 0
});

promotions.data?.promotions?.forEach(promo => {
  console.log(`${promo.name} (${promo.type}): ${promo.startDateTime} — ${promo.endDateTime}`);
});

Get Promotion Details

typescript
const details = await sdk.promotion.getPromotionsDetails({
  promotionIDs: '1854,1852,1851'
});

details.data?.promotions?.forEach(promo => {
  console.log(`${promo.name}:`);
  console.log(`  In promo: ${promo.inPromoActionTotal} products`);
  console.log(`  Available: ${promo.notInPromoActionTotal} products`);
});

Add Products to Promotions

typescript
// Check eligible products first
const eligible = await sdk.promotion.getPromotionsNomenclatures({
  promotionID: 1854,
  inAction: false,  // false = not yet added, can be added
  limit: 100,
  offset: 0
});

console.log(`Eligible products: ${eligible.data?.nomenclatures?.length}`);

// Add products
const result = await sdk.promotion.createPromotionsUpload({
  data: {
    promotionID: 1854,
    uploadNow: true,        // true = apply now, false = on promo start
    nomenclatures: [123456, 789012]
  }
});
console.log(`Upload task: ${result.data?.uploadID}`);

Combine Promotions with Advertising

Participating in WB marketplace promotions while running advertising campaigns can amplify visibility. Products in promotions get organic boosts — adding paid advertising on top increases conversions.


Rate Limit Strategy

Rate Limits Reference

MethodRate LimitInterval
Most campaign operations5 req/sec200ms
createCampaign()5 req/min12s
createBudgetDeposit()1 req/sec1s
getAdvFullstats()3 req/min20s
createAdvStat()1 req/min60s
getBidsMinV2()20 req/min3s
getAdvBalance()60 req/min
getAdvBudget()240 req/min
getCampaignCount()300 req/min
getAdvertsV2()300 req/min

Rate Limit Error Handling

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

async function safeApiCall<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (error instanceof RateLimitError) {
        const waitTime = error.retryAfter || 21000;
        console.log(`Rate limited (attempt ${attempt + 1}), waiting ${waitTime}ms`);
        await delay(waitTime);
        continue;
      }
      throw error;
    }
  }
  throw new Error('Max retries exceeded');
}

// Usage
const stats = await safeApiCall(() =>
  sdk.promotion.getAdvFullstats({
    ids: '12345',
    beginDate: '2026-01-01',
    endDate: '2026-01-31'
  })
);

Error Handling & Troubleshooting

Common Errors

ErrorCauseSolution
Validation error on budget depositCampaign not in status 11Pause campaign first: pauseCampaign()
Validation error on phrasesCampaign not active (status 9)Start campaign first: startCampaign()
Auth error on write operationsAPI key lacks advertising permissionsRequest elevated permissions from WB portal
Campaign created with status 7Product not eligible for advertisingCheck product availability and stock
Cannot delete campaignCampaign not in status 4Use getAdvStop() first
getAdvFullstats() returns empty arrayCampaign had no activity in periodVerify dates and campaign status (7/9/11)
Rate limit 429 errorsToo many requestsImplement delays (see Rate Limit Strategy)

Comprehensive Error Handling

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

async function safeCampaignOperation(sdk: WildberriesSDK, campaignId: number) {
  try {
    await sdk.promotion.startCampaign(campaignId);
    console.log('Campaign started');
  } catch (error) {
    if (error instanceof ValidationError) {
      console.error(`Invalid operation: ${error.message}`);
      // Check campaign status and retry with correct state
    } else if (error instanceof AuthenticationError) {
      console.error('API key lacks required permissions');
      // Alert admin about permission issue
    } else if (error instanceof RateLimitError) {
      console.error(`Rate limited, retry after ${error.retryAfter}ms`);
      await delay(error.retryAfter);
      // Retry the operation
    } else if (error instanceof NetworkError) {
      console.error('Network error, check connectivity');
      // Implement retry with exponential backoff
    } else if (error instanceof WBAPIError) {
      console.error(`API error ${error.statusCode}: ${error.message}`);
    } else {
      throw error;
    }
  }
}

Complete Workflow Example

End-to-end campaign management with proper error handling and rate limiting:

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

const sdk = new WildberriesSDK({ apiKey: process.env.WB_API_KEY! });
const delay = (ms: number) => new Promise(r => setTimeout(r, ms));

async function runCampaignWorkflow() {
  let campaignId: number | null = null;

  try {
    // ── Step 1: Check Balance ──
    const balance = await sdk.promotion.getAdvBalance();
    console.log(`\n=== Account Balance ===`);
    console.log(`Cabinet: ${balance.net}₽, Bonus: ${balance.bonus}₽`);
    if (balance.net < 1000) {
      throw new Error(`Insufficient balance: ${balance.net}₽ (need 1000₽)`);
    }
    await delay(1000);

    // ── Step 2: Create Campaign ──
    console.log(`\n=== Creating Campaign ===`);
    campaignId = await sdk.promotion.createCampaign({
      name: 'SDK Best Practices Demo',
      nms: [168120815],
      bid_type: 'manual',
      payment_type: 'cpm',
      placement_types: ['search']
    }) as number;
    console.log(`Created campaign: ${campaignId}`);
    await delay(5000);

    // ── Step 3: Deposit Budget ──
    console.log(`\n=== Depositing Budget ===`);
    await sdk.promotion.createBudgetDeposit(
      { sum: 1000, type: 1 },
      { id: campaignId }
    );
    const budget = await sdk.promotion.getAdvBudget({ id: campaignId });
    console.log(`Budget: ${budget.total}₽`);
    await delay(5000);

    // ── Step 4: Check Minimum Bids ──
    console.log(`\n=== Checking Bids ===`);
    const minBids = await sdk.promotion.getBidsMinV2({
      advert_id: campaignId,
      nm_ids: [168120815],
      payment_type: 'cpm',
      placement_types: ['search']
    });
    const minBid = minBids.bids[0]?.bid || 100;
    console.log(`Minimum bid: ${minBid}₽`);
    await delay(5000);

    // ── Step 5: Set Bids ──
    console.log(`\n=== Setting Bids ===`);
    await sdk.promotion.updateBids({
      bids: [{
        advert_id: campaignId,
        nm_bids: [{
          nm_id: 168120815,
          bid_kopecks: (minBid + 50) * 100,  // Bid slightly above minimum (kopecks)
          placement: 'search'
        }]
      }]
    });
    console.log(`Bid set: ${minBid + 50}₽`);
    await delay(5000);

    // ── Step 6: Start Campaign ──
    console.log(`\n=== Starting Campaign ===`);
    await sdk.promotion.startCampaign(campaignId);
    console.log('Campaign started!');
    await delay(5000);

    // ── Step 7: Set Minus-Phrases ──
    console.log(`\n=== Managing Keywords ===`);
    await sdk.promotion.setMinusPhrases({
      advert_id: campaignId,
      nm_id: 168120815,
      norm_queries: ['cheap', 'used', 'broken', 'fake']
    });
    console.log('Minus-phrases set');
    await delay(5000);

    // ── Step 8: Monitor Performance ──
    console.log(`\n=== Campaign Performance ===`);
    // Wait for campaign to accumulate data (in production, poll periodically)
    const stats = await sdk.promotion.getAdvFullstats({
      ids: String(campaignId),
      beginDate: new Date(Date.now() - 7 * 86400000).toISOString().split('T')[0],
      endDate: new Date().toISOString().split('T')[0]
    });

    if (stats.length > 0) {
      const s = stats[0];
      console.log(`Views: ${s.views}, Clicks: ${s.clicks}, CTR: ${s.ctr}%`);
      console.log(`Orders: ${s.orders}, Spent: ${s.sum}₽`);
      console.log(`CPC: ${s.cpc}₽, Conversion: ${s.cr}%`);

      // ── Step 9: Optimize Based on Data ──
      if (s.ctr < 1.0) {
        console.log('\n⚠️ Low CTR — consider adjusting bid or adding excluded phrases');
      }
      if (s.orders > 0 && s.sum / s.orders > 500) {
        console.log('\n⚠️ High CPA — consider reducing bid');
      }
    }

  } catch (error) {
    if (error instanceof RateLimitError) {
      console.error(`Rate limited. Retry after: ${error.retryAfter}ms`);
    } else if (error instanceof ValidationError) {
      console.error(`Validation error: ${error.message}`);
    } else {
      console.error('Unexpected error:', error);
    }
  } finally {
    // ── Cleanup ──
    if (campaignId) {
      try {
        await sdk.promotion.pauseCampaign(campaignId);
        await delay(2000);
        await sdk.promotion.getAdvStop({ id: campaignId });
        console.log(`\nCampaign ${campaignId} stopped`);
      } catch {
        console.log('Cleanup: campaign may already be stopped');
      }
    }
  }
}

runCampaignWorkflow();

See Also

Made with ❤️ for the Wildberries developer community