Разбираю архитектуру интеграции магазина на WordPress + WooCommerce с YCP — протоколом Яндекса для покупок через Алису AI, Поиск и Ритм
В конце мая 2026 Яндекс открыл Yandex Commerce Protocol для всех — теперь любой онлайн-магазин может подключить продажи через Алису AI, Поиск и рекомендательную ленту Ритм. Из коробки готовые решения есть для Яндекс KIT, Яндекс Маркета и 1С-Битрикс. Для всего остального — API.
У меня магазин на WooCommerce, и решения «нажать кнопку и заработало» для меня не было. Я мог либо ждать, пока Яндекс или кто-то ещё сделает интеграцию для WordPress (с непредсказуемым ETA), либо разобраться с API и написать плагин сам. Выбрал второе. На разработку ушло около двух недель работы по вечерам, плюс ещё неделя на отладку с тестовым кабинетом Яндекса. По итогу получился open-source плагин на GPL-2.0, в котором закрыты все 10 эндпоинтов спецификации YCP v1: github.com/perfinn/YCP-Yandex-Commerce-Woocommerce. Официальный сайт релиза
В статье разберу не «как поставить» (это в README), а как технически устроена интеграция: какие эндпоинты протокол требует реализовать, как маппить заказы из Яндекса в WC, где можно наступить на грабли (статусы заказов, письма, HPOS-совместимость), и почему я добавил идемпотентность по session_id с самого начала. Может, кому-то поможет либо взять плагин под свой проект, либо написать аналог под другую платформу.
Что вообще делает YCP с точки зрения архитектуры
Чтобы было понятно, что мы реализуем — короткий обзор протокола.
YCP — это HTTPS REST API от Яндекса к вашему сайту. Не наоборот. То есть когда покупатель в Алисе или Поиске нажимает «Купить в 1 клик», запросы летят от инфраструктуры Яндекса к серверу магазина. Магазин должен предоставить набор эндпоинтов, через которые Яндекс получает информацию о товарах, рассчитывает доставку, оформляет заказ и потом синхронизирует его статус.
В отличие от классических маркетплейс-интеграций (где магазин выгружает фид и забывает), тут двусторонний live-протокол. Каждый раз, когда пользователь видит товар в результатах Алисы — Яндекс делает запрос к магазину «уточни актуальную цену и наличие». Каждый раз, когда что-то меняется со статусом заказа — синхронизация через API.
Это значит, что плагин должен держать REST API всегда доступным, быстрым и идемпотентным. И тут начинается интересное.
Структура плагина
Прежде чем разбирать конкретные эндпоинты, покажу общий скелет. WordPress-плагин строится по стандартной схеме: главный PHP-файл с заголовком плагина, классы под отдельные ответственности, action/filter хуки для интеграции с WP и WooCommerce.
yandex-ycp-woo/ ├── yandex-ycp-woo.php # Главный файл, хуки активации, регистрация ├── includes/ │ ├── class-ycp-rest.php # Регистрация REST API эндпоинтов │ ├── class-ycp-auth.php # Bearer-токен авторизация │ ├── class-ycp-checkout.php # Логика создания заказа из Яндекса │ ├── class-ycp-delivery.php # Варианты доставки (СДЭК, ПВЗ) │ ├── class-ycp-orders.php # Маппинг товаров, обновление статусов │ ├── class-ycp-logger.php # Лог в WP option (для отладки) │ └── class-ycp-admin.php # Страница настроек в админке ├── assets/ │ └── admin.css └── readme.txt # Стандарт WordPress.org
Регистрация эндпоинтов идёт через стандартный для WP механизм register_rest_route в action rest_api_init:
add_action( 'rest_api_init', function() { $namespace = 'ycp/v1'; // GET /ycp/v1/warehouses register_rest_route( $namespace, '/warehouses', [ 'methods' => 'GET', 'callback' => [ 'YCP_Rest', 'get_warehouses' ], 'permission_callback' => [ 'YCP_Auth', 'check_bearer' ], ]); // POST /ycp/v1/checkout register_rest_route( $namespace, '/checkout', [ 'methods' => 'POST', 'callback' => [ 'YCP_Rest', 'create_checkout' ], 'permission_callback' => [ 'YCP_Auth', 'check_bearer' ], ]); // ... ещё 8 эндпоинтов });
Здесь важный момент с permission_callback. Это не опциональная штука, это обязательный механизм безопасности WP REST API. Если поставить туда __return_true, WordPress будет ругаться, и эндпоинт станет публично доступным без авторизации. Что для платёжного API недопустимо.
Авторизация в YCP идёт по Bearer-токену, который Яндекс генерирует в кабинете и присылает в каждом запросе:
class YCP_Auth { public static function check_bearer( WP_REST_Request $request ) { $stored_token = get_option( 'ycpy_bearer_token' ); if ( empty( $stored_token ) ) { return new WP_Error( 'no_token_configured', 'Token not set', [ 'status' => 503 ] ); } // Основной заголовок — Authorization: Bearer ... $auth = $request->get_header( 'authorization' ); if ( $auth && stripos( $auth, 'Bearer ' ) === 0 ) { $token = trim( substr( $auth, 7 ) ); if ( hash_equals( $stored_token, $token ) ) { return true; } } // Fallback на X-API-Key — иногда так удобнее тестировать через curl $api_key = $request->get_header( 'x_api_key' ); if ( $api_key && hash_equals( $stored_token, $api_key ) ) { return true; } return new WP_Error( 'unauthorized', 'Invalid token', [ 'status' => 401 ] ); } }
Использование hash_equals вместо обычного === — это защита от timing attacks. Для платёжного API такая мелочь критична, и WordPress-разработчики часто её забывают.
Эндпоинт 1: warehouses
Самый простой. Яндекс при подключении магазина первым делом запрашивает список ваших складов. Из админки настройки нашего плагина я даю заполнить только один основной склад — для большинства мелких магазинов на WooCommerce этого достаточно.
public static function get_warehouses( WP_REST_Request $request ) { $warehouse = [ 'id' => 'main', 'name' => get_option( 'ycpy_warehouse_name', 'Основной склад' ), 'address' => [ 'country' => 'RU', 'city' => get_option( 'ycpy_warehouse_city' ), 'street' => get_option( 'ycpy_warehouse_street' ), 'building' => get_option( 'ycpy_warehouse_building' ), ], 'phone' => get_option( 'ycpy_warehouse_phone' ), ]; return rest_ensure_response([ 'warehouses' => [ $warehouse ], ]); }
Тут ничего сложного, но обращу внимание на rest_ensure_response() — это WP-обёртка, которая гарантирует правильный формат ответа с заголовками. Без неё может улететь голый array, и Яндекс не распарсит.
Эндпоинт 2: basket/check — проверка корзины
Вот это уже интересно. Когда пользователь смотрит товар в Алисе и нажимает «Купить», Яндекс перед показом универсального чекаута спрашивает магазин: «у нас в корзине вот эти товары — они актуальные, цены те же, наличие есть, габариты для расчёта доставки какие?».
public static function basket_check( WP_REST_Request $request ) { $body = $request->get_json_params(); $items = $body['items'] ?? []; $checked_items = []; $unavailable = []; foreach ( $items as $item ) { $product = self::find_product( $item['id'] ); if ( ! $product || ! $product->is_in_stock() ) { $unavailable[] = [ 'id' => $item['id'], 'reason' => 'out_of_stock' ]; continue; } $checked_items[] = [ 'id' => $item['id'], 'price' => self::format_money( $product->get_price() ), 'currency' => 'RUB', 'quantity' => $item['quantity'], 'available' => $product->get_stock_quantity() ?? 999, 'dimensions' => self::get_product_dimensions( $product ), ]; } return rest_ensure_response([ 'items' => $checked_items, 'unavailable' => $unavailable, ]); }
Маппинг товаров — отдельная история. Яндекс присылает id товара, и непонятно, что это: SKU в WC или числовой post_id WooCommerce-продукта. Я сделал двойной поиск:
private static function find_product( $id ) { // 1. Пробуем как SKU (артикул) $product_id = wc_get_product_id_by_sku( $id ); if ( $product_id ) { return wc_get_product( $product_id ); } // 2. Если ID числовой — пробуем как post_id if ( is_numeric( $id ) ) { $product = wc_get_product( (int) $id ); if ( $product && $product->exists() ) { return $product; } } return null; }
Это гибкое поведение — мерчант может в фиде Яндекса указывать что удобнее: SKU или числовой ID WC-товара. Главное, чтобы при маппинге в обратную сторону всё нашлось.
С габаритами тоже есть нюанс. WooCommerce из коробки даёт поля weight, length, width, height — но многие магазины их не заполняют. А без габаритов Яндекс не сможет посчитать стоимость доставки СДЭК, и заказ не оформится. Я добавил дефолтные значения для товаров без указанных размеров:
private static function get_product_dimensions( $product ) { $weight = $product->get_weight() ?: get_option( 'ycpy_default_weight', 0.5 ); $length = $product->get_length() ?: get_option( 'ycpy_default_length', 15 ); $width = $product->get_width() ?: get_option( 'ycpy_default_width', 10 ); $height = $product->get_height() ?: get_option( 'ycpy_default_height', 5 ); return [ 'weight' => (float) $weight, 'length' => (float) $length, 'width' => (float) $width, 'height' => (float) $height, ]; }
Дефолты задаются в админке плагина — это компромисс между «магазин обязан заполнить размеры всех товаров» и «давайте просто что-то отдадим, чтобы заказ хотя бы прошёл». Я выбираю «лучше отдать дефолт и предупредить мерчанта», чем «отказать в покупке».
Эндпоинт 3: checkout — создание заказа
Самый ответственный эндпоинт. Это когда Яндекс реально хочет создать у нас в WC новый заказ. И тут самая большая ловушка, на которую я наступил во время разработки.
Ловушка 1: письма «Новый заказ на 0 ₽».
Если просто создавать заказ в WC через wc_create_order() со статусом pending, WooCommerce немедленно срабатывает на хуки order-created и шлёт письмо администратору магазина «Новый заказ #1234 на сумму 0 ₽». Почему 0? Потому что Яндекс ещё не прислал финальную сумму — она будет известна позже, после расчёта доставки и применения промокодов.
Решение — создавать заказ в нестандартном статусе checkout-draft, который WC использует для «висящих» корзин:
public static function create_checkout( WP_REST_Request $request ) { $body = $request->get_json_params(); $session_id = $body['session_id'] ?? ''; // Идемпотентность: проверяем, не создан ли уже заказ по этой сессии $existing = self::find_order_by_session( $session_id ); if ( $existing ) { return rest_ensure_response([ 'order_id' => $existing->get_id(), 'status' => 'exists', ]); } // Создаём заказ в специальном статусе draft $order = wc_create_order( [ 'status' => 'checkout-draft', ] ); // Сохраняем session_id для последующего поиска $order->update_meta_data( '_ycp_session_id', $session_id ); $order->update_meta_data( '_ycp_origin', 'yandex' ); // Добавляем товары из запроса foreach ( $body['items'] as $item_data ) { $product = self::find_product( $item_data['id'] ); if ( $product ) { $order->add_product( $product, $item_data['quantity'] ); } } // Адрес доставки if ( ! empty( $body['delivery_address'] ) ) { $order->set_address( self::map_address( $body['delivery_address'] ), 'shipping' ); } $order->calculate_totals(); $order->save(); return rest_ensure_response([ 'order_id' => $order->get_id(), 'status' => 'created', 'session_id' => $session_id, ]); }
Статус checkout-draft WC не показывает на странице «Заказы» по умолчанию (это технический статус), не шлёт писем, и не считает заказ «реально оформленным». Только когда придёт запрос на placed, мы переведём заказ в pending, и вот тогда WC отработает все стандартные хуки.
Ловушка 2: идемпотентность.
API Яндекса гарантирует at-least-once delivery — то есть запрос может прийти дважды, например если у магазина был timeout на ответ. Если просто создавать заказ каждый раз, получим два дубля одного и того же заказа.
Решение — поиск по session_id перед созданием:
private static function find_order_by_session( $session_id ) { if ( empty( $session_id ) ) { return null; } // HPOS-совместимый запрос $orders = wc_get_orders([ 'limit' => 1, 'meta_key' => '_ycp_session_id', 'meta_value' => $session_id, 'status' => [ 'checkout-draft', 'pending', 'processing', 'on-hold' ], ]); return ! empty( $orders ) ? $orders[0] : null; }
Тут второй важный момент — HPOS (High-Performance Order Storage). WooCommerce с версии 8.0 поддерживает новое хранилище заказов на основе кастомных таблиц вместо wp_posts. На новых сайтах оно включено по умолчанию. Если писать запросы через прямой SQL к wp_postmeta (как многие старые плагины), на HPOS-сайтах ничего не найдётся.
Поэтому я использую везде высокоуровневые wc_get_orders(), $order->update_meta_data(), $order->get_meta() — они работают в обоих хранилищах. Это требует чуть больше дисциплины, но плагин становится совместимым «навсегда».
Эндпоинт 4: checkout/placed — заказ подтверждён
Когда пользователь нажал «Оплатить» в виджете Яндекса, прилетает placed. Финальная сумма, выбранный способ доставки, статус оплаты — всё уже сформировано на стороне Яндекса. Наша задача — перевести WC-заказ из checkout-draft в нормальный статус.
public static function checkout_placed( WP_REST_Request $request ) { $body = $request->get_json_params(); $session_id = $body['session_id'] ?? ''; $order = self::find_order_by_session( $session_id ); if ( ! $order ) { return new WP_Error( 'order_not_found', 'No draft order for session', [ 'status' => 404 ] ); } // Записываем финальные данные $order->update_meta_data( '_ycp_yandex_order_id', $body['yandex_order_id'] ); $order->update_meta_data( '_ycp_payment_method', $body['payment_method'] ); $order->set_total( $body['total_amount'] ); // Способ оплаты определяет статус if ( $body['payment_status'] === 'paid' ) { // Оплата прошла на стороне Яндекса (Яндекс Пэй, Сплит) → processing $order->update_status( 'processing', 'Оплачено через Яндекс Чекаут' ); } else { // Наложенный платёж → pending (магазин обработает сам) $order->update_status( 'pending', 'Заказ оформлен, оплата при получении' ); } return rest_ensure_response([ 'status' => 'ok', 'order_id' => $order->get_id(), ]); }
Когда статус заказа переходит из checkout-draft в pending или processing, WooCommerce автоматически отправляет администратору письмо с правильной финальной суммой. Это и есть тот «флоу checkout-draft → pending», который решает проблему пустых писем.
Эндпоинты 5–10: остальная синхронизация
Чтобы не превращать статью в reference manual, остальные эндпоинты опишу кратко:
POST /checkout/cancel — пользователь закрыл виджет, не оформив. Удаляем draft-заказ или помечаем его как cancelled.
POST /checkout/delivery/options — возвращаем варианты доставки. Тут я подтягиваю активные методы из WC Shipping, плюс отдельно поддерживаю «самовывоз из ПВЗ».
GET /checkout/delivery/pickup_points — для магазинов с собственными ПВЗ. Я добавил в админке простой UI «список ПВЗ магазина» с CRUD-операциями.
GET /order — Яндекс синхронизирует статусы. Возвращаем историю изменений статуса заказа.
POST /order/cancel — отмена оформленного заказа. Переводим WC-заказ в cancelled, восстанавливаем сток.
POST /order/delivered — заказ доставлен. Переводим заказ в completed.
Все эти эндпоинты идемпотентны по тому же session_id или yandex_order_id, чтобы повторные запросы не делали ничего вредного.
Логирование: главный инструмент отладки
Когда работаешь с внешним API, без логов жить нельзя. На моей стороне нет способа повторить запрос Яндекса вручную — я могу только реагировать на то, что прилетело. Поэтому логирование критично.
Я не стал тащить файловую запись или БД — для маленьких магазинов это лишний overhead. Сделал лог в WP option с ротацией по последним 100 записям:
class YCP_Logger { const OPTION_NAME = 'ycpy_log'; const MAX_ENTRIES = 100; public static function log( $type, $endpoint, $request_body, $response, $error = null ) { $log = get_option( self::OPTION_NAME, [] ); array_unshift( $log, [ 'timestamp' => current_time( 'mysql' ), 'type' => $type, // REQUEST, RESPONSE, EXCEPTION 'endpoint' => $endpoint, 'request_body' => is_string( $request_body ) ? $request_body : wp_json_encode( $request_body ), 'response' => is_string( $response ) ? $response : wp_json_encode( $response ), 'error' => $error, ]); // Оставляем только последние N записей if ( count( $log ) > self::MAX_ENTRIES ) { $log = array_slice( $log, 0, self::MAX_ENTRIES ); } update_option( self::OPTION_NAME, $log, false ); // autoload = false } }
Вызывается перед каждым ответом эндпоинта, плюс из глобального try-catch на случай исключений. В админке плагина — таблица с фильтрацией по типу. Когда мерчант приходит с проблемой «заказ не оформляется», первое, что я прошу — открыть лог и найти запись с типом EXCEPTION. Чаще всего там сразу видно, что прислал Яндекс и где упало.
autoload = false — важный нюанс. По умолчанию WP-опции загружаются вместе с каждым запросом к сайту. Если лог огромный, это замедлит каждую страницу. Отключаем autoload — опция читается только когда явно запрошена.
Чему я научился за это время
Несколько уроков из разработки.
WC HPOS — это будущее, и игнорировать его нельзя. Если бы я писал плагин «по-старому» через $wpdb->postmeta, он бы не работал у половины современных магазинов. Все обращения к данным заказов — только через WC API.
Идемпотентность с первого дня. Я мог бы добавить проверку на дубли «когда-нибудь потом». В реальности это бы означало пачку дублированных заказов на боевых магазинах в первый же день. Делай идемпотентность с первого коммита.
Локальные дефолты для нестабильных данных. Магазины — это всегда грязные данные. Половина товаров без габаритов, четверть — без артикулов, в каждом десятом — пустые описания. Плагин, который требует «все товары должны быть идеально заполнены», не взлетит. Плагин с разумными дефолтами и понятными warning-ами — взлетит.
Лог в админке важнее любой документации. Когда у мерчанта что-то не работает, никто не будет лезть в FTP смотреть /wp-content/uploads/ycp-logs/. Кнопка «Лог последних запросов» прямо в админке решает 80% запросов в поддержку.
Статус checkout-draft — недооценённая фишка WooCommerce. До этой работы я о нём не знал. Это очень удобно для любых сценариев «отложенного оформления заказа» — корзины с saved later, multi-step checkout, и теперь — внешние чекауты вроде Яндекса.
Что дальше
Плагин сейчас закрывает все 10 эндпоинтов YCP v1 и работает на нескольких боевых магазинах, включая мой. Из планов:
Поддержка multi-warehouse (Яндекс это поддерживает, но я пока вынес наружу только один склад)
Интеграция с WC Multilingual для магазинов на нескольких языках
Bulk-операции по массовому экспорту товаров в Яндекс Товары (сейчас это делается через стандартный YML-фид)
Поддержка партиал-оплат и подписочных моделей, если Яндекс их откроет в YCP v2 Если у вас тоже WooCommerce, и вы хотите подключить продажи через Алису AI — попробуйте плагин. GPL-2.0, бесплатно, поддержка по issue в репозитории: github.com/perfinn/YCP-Yandex-Commerce-Woocommerce.
PR и баг-репорты приветствую. Особенно полезно было бы получить фидбек от магазинов с большим ассортиментом (10k+ товаров) — на моих тестовых стендах таких объёмов не было, и где-то могут вылезти узкие места по производительности.
Полезные ссылки:
Официальный сайт плагина: perfinn.ru/ycp/
Репозиторий плагина: github.com/perfinn/YCP-Yandex-Commerce-Woocommerce
Документация YCP от Яндекса: merchants.yandex.ru/ycp
WooCommerce HPOS docs: woocommerce.com/document/high-performance-order-storage
