WooCommerce: синхронизация остатков через REST API с проверкой конфликтов

Сценарий, который встречается чаще всего: товар продаётся на двух сайтах, а остаток обновляется только на одном из них. В итоге один магазин уже списал склад, второй ещё показывает доступное количество и принимает заказ. Если синхронизировать остатки «в лоб», без проверки версии данных, можно легко перезаписать более свежий остаток старым значением.

Ниже — рабочая схема для WooCommerce: один сайт отправляет изменения по REST API, второй принимает их только если входящее значение не устарело. Это не «магическая синхронизация», а контролируемый обмен данными с понятной диагностикой и проверкой результата.

Когда проблема действительно в синхронизации остатков

Сначала стоит убедиться, что дело не в кэше, не в стороннем складе и не в ручных правках менеджеров. Типичные признаки именно этой проблемы:

  • остаток меняется в админке, но на другом сайте остаётся старое значение;
  • после импорта или фоновой задачи количество «откатывается» назад;
  • заказы создаются на обоих сайтах, но списание происходит только в одном источнике;
  • в логах есть запросы к REST API, но часть обновлений теряется без ошибки.

Что проверить перед внедрением

  • Отключён ли объектный кэш на время диагностики или хотя бы очищается ли он после обновления товара.
  • Не обновляет ли остатки внешний сервис: 1С, ERP, складской модуль, CSV-импорт.
  • Одинаков ли SKU у товара на обоих сайтах. Если SKU расходится, синхронизация по ID работать не будет.
  • Есть ли у товара вариации: для них нужен отдельный учёт остатков по variation ID.

Какой подход выбрать: плагин, код или гибрид

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

ПодходКогда подходитМинусы
ПлагинНужна быстрая настройка без доработокСложно учесть конфликт версий и нестандартную логику
Код через REST APIНужен контроль над правилами синхронизацииНужно поддерживать свой код и логирование
ГибридЕсть готовый обмен, но нужна защита от перезаписиИнтеграция становится сложнее в сопровождении

Пошаговая схема синхронизации остатков

1. Добавляем метку актуальности к товару

Чтобы понять, какое значение свежее, удобно хранить время последнего изменения остатка в мета-поле. Его можно обновлять при ручном изменении товара, при списании после заказа и при импорте.

add_action('woocommerce_product_set_stock', function( $product ) {
    if ( ! $product instanceof WC_Product ) {
        return;
    }

    update_post_meta( $product->get_id(), '_stock_sync_updated_at', time() );
}, 10, 1 );

add_action('woocommerce_variation_set_stock', function( $product ) {
    if ( ! $product instanceof WC_Product ) {
        return;
    }

    update_post_meta( $product->get_id(), '_stock_sync_updated_at', time() );
}, 10, 1 );

Если у вас есть собственный процесс списания, лучше обновлять эту метку именно там, а не только в хуках WooCommerce. Тогда источник истины будет один.

2. Отправляем остаток на второй сайт

Ниже пример отправки данных через wp_remote_post(). В запросе передаём SKU, остаток и метку времени изменения. Авторизация — через Application Passwords или другой безопасный способ, который уже принят в проекте.

function wpsync_send_stock_update( $product_id ) {
    $product = wc_get_product( $product_id );
    if ( ! $product ) {
        return;
    }

    $payload = array(
        'sku'       => $product->get_sku(),
        'stock'     => (int) $product->get_stock_quantity(),
        'updatedAt' => (int) get_post_meta( $product_id, '_stock_sync_updated_at', true ),
        'type'      => $product->get_type(),
    );

    $response = wp_remote_post( 'https://second-site.example/wp-json/wpsync/v1/stock', array(
        'timeout' => 15,
        'headers' => array(
            'Content-Type' => 'application/json',
            'Authorization' => 'Basic ' . base64_encode( 'api-user:app-password' ),
        ),
        'body'    => wp_json_encode( $payload ),
    ) );

    if ( is_wp_error( $response ) ) {
        error_log( 'Stock sync failed: ' . $response->get_error_message() );
    }
}

add_action( 'woocommerce_product_set_stock', function( $product ) {
    if ( $product instanceof WC_Product ) {
        wpsync_send_stock_update( $product->get_id() );
    }
}, 20 );

Для вариаций используйте тот же принцип, но отправляйте $variation->get_id() и SKU вариации. Не смешивайте родительский товар и вариацию в одном потоке, иначе легко получить неверный остаток на витрине.

3. Принимаем обновление только если оно свежее

На принимающем сайте регистрируем REST-роут и сравниваем входящую метку времени с локальной. Если пришло старое значение, запрос отклоняем и пишем причину в лог.

add_action( 'rest_api_init', function() {
    register_rest_route( 'wpsync/v1', '/stock', array(
        'methods'             => 'POST',
        'callback'            => 'wpsync_receive_stock_update',
        'permission_callback' => function() {
            return current_user_can( 'manage_woocommerce' );
        },
    ) );
} );

function wpsync_receive_stock_update( WP_REST_Request $request ) {
    $sku       = sanitize_text_field( $request->get_param( 'sku' ) );
    $stock     = (int) $request->get_param( 'stock' );
    $updatedAt = (int) $request->get_param( 'updatedAt' );

    if ( ! $sku || $updatedAt <= 0 ) {
        return new WP_REST_Response( array(
            'success' => false,
            'message' => 'Invalid payload',
        ), 400 );
    }

    $product_id = wc_get_product_id_by_sku( $sku );
    if ( ! $product_id ) {
        return new WP_REST_Response( array(
            'success' => false,
            'message' => 'Product not found',
        ), 404 );
    }

    $local_updated_at = (int) get_post_meta( $product_id, '_stock_sync_updated_at', true );
    if ( $local_updated_at > $updatedAt ) {
        return new WP_REST_Response( array(
            'success' => false,
            'message' => 'Stale update rejected',
        ), 409 );
    }

    $product = wc_get_product( $product_id );
    $product->set_manage_stock( true );
    $product->set_stock_quantity( $stock );
    $product->save();

    update_post_meta( $product_id, '_stock_sync_updated_at', $updatedAt );

    return new WP_REST_Response( array(
        'success' => true,
        'product_id' => $product_id,
    ), 200 );
}

Как проверить, что синхронизация работает

Проверка должна быть не «на глаз», а по конкретным признакам. Сначала меняем остаток на первом сайте, затем смотрим ответ API и состояние товара на втором.

  • В ответе API должен быть 200 для актуального обновления и 409 для устаревшего.
  • В карточке товара на втором сайте должен измениться остаток и время последнего обновления.
  • Если включён лог запросов, в нём должна быть видна причина отказа при конфликте.
  • Для вариаций проверяйте именно страницу вариации, а не только родительский товар.

Удобный тестовый сценарий: вручную поменяйте остаток на сайте A, сразу после этого — на сайте B, но с более старой меткой времени. Если защита работает, сайт B не перезапишет более свежее значение.

Частые ошибки и как их исправить

Синхронизация идёт, но остатки «скачут»

Обычно это означает, что в систему одновременно пишут два источника: ручное редактирование в админке и внешний импорт. Нужно выбрать один источник истины или хотя бы развести потоки по времени. Иначе любое обновление будет перетирать предыдущее.

Товар не находится по SKU

Причина часто в том, что SKU пустой, не уникальный или отличается между сайтами. Для синхронизации по SKU это критично. Если SKU нельзя сделать стабильным, придётся строить сопоставление по отдельной таблице соответствий.

REST-запросы возвращают 401 или 403

Проверьте авторизацию, права пользователя и то, не блокирует ли запрос WAF или security-плагин. Иногда проблема не в WooCommerce, а в том, что сервер режет заголовок Authorization.

Остаток обновился, но на витрине старое значение

Это уже похоже на кэш. Нужно очистить кэш страницы, объектный кэш и, если используется CDN, проверить правила инвалидации. Без этого синхронизация в базе будет работать, а фронтенд — показывать старые данные.

Безопасность и производительность: что не стоит упускать

Если синхронизация идёт между сайтами по HTTP-запросам, не отправляйте данные без защиты. Минимум — HTTPS, отдельный пользователь с ограниченными правами и понятный журнал ошибок. Если есть возможность, ограничьте доступ к REST-роуту по IP или дополнительному токену.

По производительности важно не слать запрос на каждый чих. Если у вас массовый импорт, лучше собирать изменения в очередь и отправлять пачками через WP-Cron или отдельный фоновый процесс. Иначе при большом каталоге вы получите длинные цепочки синхронных запросов и лишнюю нагрузку на админку.

Для проектов, где нужно ещё и чистить лишние дубли, мета-данные и мусор после интеграций, имеет смысл отдельно проверить инструменты оптимизации вроде Clearfy Pro: https://wpshop.ru/plugins/clearfy. Но саму логику синхронизации остатков всё равно лучше держать в коде, чтобы не зависеть от скрытых правил плагина.

Короткий чек-лист перед запуском в продакшн

  • SKU одинаковый и уникальный на всех сайтах.
  • Есть метка времени последнего изменения остатка.
  • REST-роут отклоняет устаревшие данные с кодом 409.
  • Проверена авторизация и HTTPS.
  • Протестированы простые товары и вариации.
  • Проверено поведение кэша после обновления.

Если нужен именно обмен данными между несколькими сайтами, эта схема даёт предсказуемое поведение: вы видите, что пришло, что было отклонено и почему. Для WooCommerce это обычно важнее, чем «автоматизация любой ценой».

Как синхронизировать данные WooCommerce при использовании кэширования
31.07.2026
Как удалить неиспользуемые плагины в WordPress без риска для сайта
26.11.2025
Как отключить автоматические обновления WordPress и плагинов
15.04.2026
Ошибка 429 «Слишком много запросов» в WooCommerce: причины и решение при синхронизации складских данных
29.06.2026
WooCommerce: как реализовать отмену и возврат заказов с собственной логикой
19.06.2026