Главная/Статьи/Wildberries API — синхронизация товаров и заказов

Wildberries API — синхронизация товаров и заказов

Интеграция с API Wildberries: получение остатков, обновление цен, загрузка заказов. Обходим лимиты, обрабатываем ошибки, настраиваем автосинхронизацию.

ДМ
Дмитрий Мещеряков
📅 14 августа 2026 г.📖 9 мин чтения

Интеграция с Wildberries отличается от большинства API одной особенностью: это несколько разных сервисов с разными базовыми адресами, разными лимитами и разной логикой ответов, объединённых общим токеном. Держать это в голове нужно с самого начала — универсальный клиент «на все случаи» здесь получается хуже, чем несколько специализированных.

Разберём реализацию и то, что определяет надёжность обмена с маркетплейсом.

Архитектура Wildberries API

Wildberries имеет несколько API:

  • Контент API — создание/редактирование карточек товаров
  • Marketplace API — цены, остатки, заказы, поставки
  • Статистика API — отчёты, продажи, аналитика
  • Рекламные API — управление рекламой

Каждый API требует свой токен, полученный в личном кабинете.

💡 Совет

Что реализуем: Синхронизацию остатков с 1С/CMS → WB, получение новых заказов WB → CRM, автообновление цен по расписанию.

Шаг 1: HTTP-клиент для WB API

php
<?php
// src/Wildberries/Client.php
namespace App\Wildberries;
use GuzzleHttp\Client as HttpClient;
use GuzzleHttp\Exception\RequestException;
use Psr\Log\LoggerInterface;
class Client
{
    private HttpClient $http;
    private string $token;
    private LoggerInterface $logger;
    // Базовые URL разных API
    private const ENDPOINTS = [
        'content'     => 'https://content-api.wildberries.ru',
        'marketplace' => 'https://marketplace-api.wildberries.ru',
        'statistics'  => 'https://statistics-api.wildberries.ru',
        'advert'      => 'https://advert-api.wildberries.ru',
    ];
    public function __construct(
        string $token,
        LoggerInterface $logger
    ) {
        $this->token = $token;
        $this->logger = $logger;
        $this->http = new HttpClient([
            'timeout' => 30,
            'headers' => [
                'Authorization' => $token,
                'Content-Type' => 'application/json',
            ],
        ]);
    }
    /**
     * GET-запрос к API
     */
    public function get(
        string $api,
        string $path,
        array $query = []
    ): array {
        return $this->request('GET', $api, $path, ['query' => $query]);
    }
    /**
     * POST-запрос к API
     */
    public function post(
        string $api,
        string $path,
        array $data = []
    ): array {
        return $this->request('POST', $api, $path, ['json' => $data]);
    }
    /**
     * Базовый метод запроса с retry-логикой
     */
    private function request(
        string $method,
        string $api,
        string $path,
        array $options = []
    ): array {
        $url = self::ENDPOINTS[$api] . $path;
        $attempts = 0;
        $maxAttempts = 3;
        while ($attempts < $maxAttempts) {
            try {
                $response = $this->http->request($method, $url, $options);
                $body = $response->getBody()->getContents();
                return json_decode($body, true) ?? [];
            } catch (RequestException $e) {
                $attempts++;
                $code = $e->getResponse()?->getStatusCode() ?? 0;
                // 429 = Rate limit — ждём и повторяем
                if ($code === 429 && $attempts < $maxAttempts) {
                    $retryAfter = $e->getResponse()
                        ->getHeaderLine('Retry-After') ?: 60;
                    $this->logger->warning("WB API rate limit, waiting {$retryAfter}s");
                    sleep((int) $retryAfter);
                    continue;
                }
                // Логируем и пробрасываем ошибку
                $this->logger->error("WB API error", [
                    'url' => $url,
                    'code' => $code,
                    'message' => $e->getMessage(),
                    'attempt' => $attempts,
                ]);
                if ($attempts >= $maxAttempts) {
                    throw $e;
                }
                sleep(2 ** $attempts); // Exponential backoff
            }
        }
        return [];
    }
}

Шаг 2: Синхронизация остатков

php
<?php
// src/Wildberries/StockService.php
namespace App\Wildberries;
class StockService
{
    private Client $client;
    public function __construct(Client $client)
    {
        $this->client = $client;
    }
    /**
     * Получение остатков со склада WB
     */
    public function getStocks(int $warehouseId): array
    {
        $response = $this->client->post('marketplace', '/api/v3/stocks/' . $warehouseId);
        return $response['stocks'] ?? [];
    }
    /**
     * Обновление остатков на складе WB
     * 
     * @param int $warehouseId ID склада WB
     * @param array $stocks Массив [['sku' => 'ABC123', 'amount' => 10], ...]
     */
    public function updateStocks(int $warehouseId, array $stocks): bool
    {
        // WB принимает максимум 1000 позиций за раз
        $chunks = array_chunk($stocks, 1000);
        foreach ($chunks as $chunk) {
            $this->client->put('marketplace', '/api/v3/stocks/' . $warehouseId, [
                'stocks' => $chunk
            ]);
        }
        return true;
    }
    /**
     * Синхронизация остатков из локальной БД в WB
     */
    public function syncFromDatabase(int $warehouseId): array
    {
        // Получаем товары из нашей БД
        $products = $this->getLocalProducts();
        $stocks = [];
        foreach ($products as $product) {
            $stocks[] = [
                'sku' => $product['article'],
                'amount' => max(0, $product['quantity']), // WB не принимает отрицательные
            ];
        }
        $this->updateStocks($warehouseId, $stocks);
        return [
            'synced' => count($stocks),
            'warehouse' => $warehouseId,
        ];
    }
    private function getLocalProducts(): array
    {
        // Здесь запрос к вашей БД/CMS/1С
        return [];
    }
}
⚠️ Важно

Лимиты API: WB ограничивает количество запросов. Для остатков — не более 6 запросов в минуту. Используйте пакетные обновления и очереди.

Шаг 3: Обновление цен

⚠️ Важно

Цены — самое опасное место интеграции с маркетплейсом. Ошибка в остатках стоит штрафа за отмену; ошибка в цене может стоить распроданного по себестоимости склада, причём за считанные часы и без возможности отменить заказы.

Обязательный минимум защиты перед отправкой:

Проверка на вменяемость каждой позиции. Отклонение новой цены от текущей больше чем на заданный процент — повод не отправлять, а уведомить человека. Потерянный множитель, съехавшая колонка в выгрузке, нули вместо цены — всё это выглядит как обычные числа и проходит любую проверку типов.

Проверка объёма изменений. Если в этом прогоне меняется цена у 90% каталога, это почти наверняка сбой источника, а не решение отдела продаж.

Нижняя граница по себестоимости. Жёсткая, на уровне кода, а не «менеджер проверит». Цена ниже закупочной не должна уходить в маркетплейс ни при каких обстоятельствах.

Журнал отправленных цен. С ним разбор инцидента занимает минуты; без него — сутки и разговор с бухгалтерией.

php
<?php
// src/Wildberries/PriceService.php
namespace App\Wildberries;
class PriceService
{
    private Client $client;
    public function __construct(Client $client)
    {
        $this->client = $client;
    }
    /**
     * Получение текущих цен
     */
    public function getPrices(int $limit = 1000, int $offset = 0): array
    {
        return $this->client->get('marketplace', '/api/v2/list/goods/filter', [
            'limit' => $limit,
            'offset' => $offset,
        ]);
    }
    /**
     * Обновление цен
     * 
     * @param array $prices [['nmId' => 123, 'price' => 1990], ...]
     */
    public function updatePrices(array $prices): array
    {
        // WB принимает максимум 1000 позиций
        $chunks = array_chunk($prices, 1000);
        $results = [];
        foreach ($chunks as $chunk) {
            $response = $this->client->post('marketplace', '/api/v2/upload/task', [
                'data' => array_map(fn($p) => [
                    'nmId' => $p['nmId'],
                    'price' => $p['price'],
                ], $chunk),
            ]);
            $results[] = $response;
        }
        return $results;
    }
    /**
     * Установка скидки
     */
    public function setDiscount(int $nmId, int $discount): array
    {
        return $this->client->post('marketplace', '/api/v2/upload/task', [
            'data' => [
                [
                    'nmId' => $nmId,
                    'discount' => min(99, max(0, $discount)),
                ],
            ],
        ]);
    }
    /**
     * Массовое обновление цен с учётом маржи
     */
    public function syncPricesWithMargin(float $marginPercent = 20): array
    {
        $localProducts = $this->getLocalProductsWithCost();
        $prices = [];
        foreach ($localProducts as $product) {
            $cost = $product['cost']; // Себестоимость
            $wbCommission = 0.15; // Комиссия WB ~15%
            // Рассчитываем цену с учётом комиссии и маржи
            $targetMargin = $marginPercent / 100;
            $price = $cost / (1 - $wbCommission - $targetMargin);
            // Округляем до красивой цены
            $price = $this->roundPrice($price);
            $prices[] = [
                'nmId' => $product['nm_id'],
                'price' => $price,
            ];
        }
        return $this->updatePrices($prices);
    }
    /**
     * Округление до "красивой" цены
     */
    private function roundPrice(float $price): int
    {
        if ($price < 1000) {
            return (int) ceil($price / 10) * 10 - 1; // 990, 890, etc.
        }
        if ($price < 10000) {
            return (int) ceil($price / 100) * 100 - 1; // 2990, 3990, etc.
        }
        return (int) ceil($price / 1000) * 1000 - 1; // 14990, 19990, etc.
    }
    private function getLocalProductsWithCost(): array
    {
        return [];
    }
}

Шаг 4: Получение заказов

php
<?php
// src/Wildberries/OrderService.php
namespace App\Wildberries;
use DateTime;
class OrderService
{
    private Client $client;
    public function __construct(Client $client)
    {
        $this->client = $client;
    }
    /**
     * Получение новых заказов
     */
    public function getNewOrders(?int $next = null): array
    {
        $params = ['limit' => 1000];
        if ($next) {
            $params['next'] = $next;
        }
        return $this->client->get('marketplace', '/api/v3/orders/new', $params);
    }
    /**
     * Получение всех заказов (с пагинацией)
     */
    public function getAllNewOrders(): array
    {
        $allOrders = [];
        $next = null;
        do {
            $response = $this->getNewOrders($next);
            $orders = $response['orders'] ?? [];
            $allOrders = array_merge($allOrders, $orders);
            $next = $response['next'] ?? null;
        } while ($next && count($orders) > 0);
        return $allOrders;
    }
    /**
     * Получение заказов за период (из статистики)
     */
    public function getOrdersForPeriod(DateTime $from, DateTime $to): array
    {
        // Statistics API требует отдельный токен!
        return $this->client->get('statistics', '/api/v1/supplier/orders', [
            'dateFrom' => $from->format('Y-m-d'),
            'dateTo' => $to->format('Y-m-d'),
        ]);
    }
    /**
     * Подтверждение сборки заказа
     */
    public function confirmOrder(int $orderId, array $skus): array
    {
        return $this->client->post('marketplace', '/api/v3/orders/deliver', [
            'orders' => [
                [
                    'orderId' => $orderId,
                    'skus' => $skus,
                ],
            ],
        ]);
    }
    /**
     * Отмена заказа
     */
    public function cancelOrder(int $orderId): array
    {
        return $this->client->post('marketplace', '/api/v3/orders/cancel', [
            'orderIds' => [$orderId],
        ]);
    }
    /**
     * Синхронизация заказов в локальную CRM
     */
    public function syncToCRM(): array
    {
        $orders = $this->getAllNewOrders();
        $synced = 0;
        $errors = [];
        foreach ($orders as $order) {
            try {
                $this->saveOrderToCRM($order);
                $synced++;
            } catch (\Exception $e) {
                $errors[] = [
                    'orderId' => $order['id'],
                    'error' => $e->getMessage(),
                ];
            }
        }
        return [
            'total' => count($orders),
            'synced' => $synced,
            'errors' => $errors,
        ];
    }
    private function saveOrderToCRM(array $order): void
    {
        // Сохранение в вашу CRM/БД
        // $order содержит: id, rid, createdAt, skus, price, ...
    }
}

Шаг 5: Автосинхронизация по расписанию

Laravel Schedule

php
<?php
// app/Console/Kernel.php
protected function schedule(Schedule $schedule): void
{
    // Синхронизация остатков каждые 30 минут
    $schedule->command('wb:sync-stocks')
        ->everyThirtyMinutes()
        ->withoutOverlapping();
    // Получение новых заказов каждые 5 минут
    $schedule->command('wb:sync-orders')
        ->everyFiveMinutes()
        ->withoutOverlapping();
    // Обновление цен раз в день в 6:00
    $schedule->command('wb:sync-prices')
        ->dailyAt('06:00')
        ->withoutOverlapping();
}

Artisan Command для синхронизации

php
<?php
// app/Console/Commands/SyncWildberriesStocks.php
namespace App\Console\Commands;
use App\Wildberries\StockService;
use Illuminate\Console\Command;
class SyncWildberriesStocks extends Command
{
    protected $signature = 'wb:sync-stocks {--warehouse=}';
    protected $description = 'Sync stocks to Wildberries';
    public function handle(StockService $service): int
    {
        $warehouseId = $this->option('warehouse') 
            ?? config('services.wildberries.warehouse_id');
        $this->info("Starting stock sync to warehouse {$warehouseId}...");
        try {
            $result = $service->syncFromDatabase($warehouseId);
            $this->info("Synced {$result['synced']} items");
            return Command::SUCCESS;
        } catch (\Exception $e) {
            $this->error("Sync failed: " . $e->getMessage());
            return Command::FAILURE;
        }
    }
}

Обработка ошибок API

php
<?php
// src/Wildberries/Exception/ApiException.php
namespace App\Wildberries\Exception;
class ApiException extends \Exception
{
    private ?array $response;
    public function __construct(
        string $message,
        int $code = 0,
        ?array $response = null
    ) {
        parent::__construct($message, $code);
        $this->response = $response;
    }
    public function getResponse(): ?array
    {
        return $this->response;
    }
    /**
     * Проверка, можно ли повторить запрос
     */
    public function isRetryable(): bool
    {
        return in_array($this->code, [429, 500, 502, 503, 504]);
    }
}

Итоги

Реализованы клиент с повторами и учётом лимитов, синхронизация остатков и цен, получение заказов и работа по расписанию. Что важно помнить при эксплуатации такого обмена.

Токен — полный доступ к вашему кабинету продавца. Хранить в конфигурации вне репозитория, выдавать с минимальным набором прав под конкретную задачу, отзывать при увольнении сотрудника, имевшего к нему доступ.

Повторы должны различать типы ошибок. Временный сбой сети или ответ «слишком много запросов» — повторить с увеличивающейся задержкой. Ошибка «неверные данные» — повторять бессмысленно, нужно уведомить человека. Слепой повтор на любую ошибку забивает очередь и добивает API, который и так отвечает плохо.

Обработка заказов обязана быть идемпотентной. Один и тот же заказ вы получите повторно — при перезапуске, при сбое на середине, при пересечении интервалов опроса.

Мониторинг — не «следующий шаг», а часть решения. Обмен работает в фоне и ломается молча: истёк токен, изменился формат ответа, упал сервер. Без проверки «последняя успешная синхронизация не старше часа» с уведомлением вы узнаете о проблеме от службы поддержки маркетплейса или из отчёта о штрафах.

Заложите время на изменения API. Маркетплейсы меняют версии и форматы регулярно и не всегда с заметным предупреждением. Интеграция — это не законченный проект, а система, требующая сопровождения; закладывайте это в оценку сразу.

🚀

Хотите такое же решение?

Настрою окружение под ваш проект, учту специфику инфраструктуры и обучу команду.

Обсудить проект →
Бесплатная консультация · Ответ в течение дня

Комментарии

Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.