Представьте типичную ситуацию. Интернет-магазин на WooCommerce получает 50–100 заказов в день. Менеджер вручную копирует данные из админки в 1С, потом сверяет остатки на складе, потом обновляет статусы в CRM. На всё это уходит три-четыре часа ежедневно, а ошибки всё равно случаются. Знакомо?
Решение лежит на поверхности — автоматизировать обмен данными через API. WooCommerce из коробки даёт REST API с набором стандартных эндпоинтов. Но вот незадача: встроенные точки доступа возвращают данные в формате, который редко устраивает внешние системы напрямую. Всё равно приходится писать прослойку — маппинг полей, фильтрацию, обогащение данных. Именно здесь на сцену выходят кастомные точки доступа WP REST API, и сегодня разберём, как их грамотно спроектировать и реализовать.
Зачем нужны кастомные эндпоинты, если есть встроенный API WooCommerce
Стандартный REST API WooCommerce (маршрут /wp-json/wc/v3/orders) умеет многое: отдавать заказы, создавать, обновлять статусы. Но у него есть ряд ограничений, которые становятся проблемой при интеграции с реальными внешними системами.
Что не так с дефолтным API заказов
- Лишние данные. Типичный ответ на запрос списка заказов весит 15–40 КБ на каждый объект. Внешней системе для синхронизации может понадобиться только номер, сумма, статус и список товаров — но она получает полный JSON с метаданными, данными купонов, адресами и прочим.
- Неудобная структура. ERP-системы ждут плоскую таблицу с полями вроде
order_id,customer_name,total,sku,quantity. API WooCommerce отдаёт вложенную структуру, где товары — массив объектов внутри объекта заказа. - Нет фильтрации по бизнес-логике. «Отдай все заказы за последние сутки, которые ещё не выгружались в 1С» — такого фильтра в стандартном API нет.
- Производительность. Выгрузка 500 заказов через стандартный endpoint с пагинацией по 10 штук — это 50 HTTP-запросов. Кастомный endpoint может отдать всё за один раз, в нужном формате, с оптимизированным SQL.
- Безопасность. Полный доступ к API WooCommerce — это широкие права. Для выгрузки заказов лучше дать внешней системе доступ только к read-only эндпоинту с ограниченными данными.
По сути, кастомные точки доступа — это специализированные каналы связи между вашим сайтом и внешним миром. Один endpoint — одна задача. Чисто, понятно, контролируемо.
Архитектура интеграции: как это работает
Прежде чем писать код, стоит на секунду остановиться и подумать о картине в целом. Кастомный REST API endpoint — это не просто функция, которая возвращает JSON. Это часть архитектуры, и от того, как вы её спроектируете, зависит надёжность всей системы.
Типичная схема выглядит так:
- Внешняя система (CRM, ERP, складской учёт, BI-панель) отправляет HTTP-запрос на ваш WordPress-сайт.
- WordPress маршрутизирует запрос в зарегистрированный callback через хук
rest_api_init. - Callback выполняет нужную бизнес-логику: формирует SQL-запрос, обогащает данные, применяет фильтры.
- Ответ уходит в формате JSON (или XML — некоторые ERP-системы до сих пор предпочитают его).
Важный момент: кто инициирует обмен данными. В схеме «вытягивания» (pull) внешняя система сама запрашивает данные. В схеме «проталкивания» (push) WordPress отправляет данные при смене статуса заказа через хуки WooCommerce. Оба подхода можно реализовать через кастомные endpoints — но для выгрузки чаще используют pull.
Регистрация кастомного REST API endpointа
Давайте перейдём к практике. Вот минимальный рабочий пример кастомной точки доступа, которая отдаёт список заказов в упрощённом формате для внешней системы.
Код размещается в файле functions.php дочерней темы или в отдельном плагине. Второй вариант надёжнее — при смене темы логика не потеряется.
add_action('rest_api_init', function () {
register_rest_route('custom/v1', '/orders-for-export', [
'methods' => 'GET',
'callback' => 'handle_orders_export',
'permission_callback' => 'verify_api_access',
'args' => [
'after' => [
'required' => false,
'type' => 'string',
'description' => 'Дата в формате Y-m-d, заказы после этой даты',
'sanitize_callback' => 'sanitize_text_field',
],
'status' => [
'required' => false,
'type' => 'string',
'default' => 'processing',
'sanitize_callback' => 'sanitize_text_field',
],
'per_page' => [
'required' => false,
'type' => 'integer',
'default' => 100,
'sanitize_callback' => 'absint',
],
],
]);
});
Что здесь происходит. Мы регистрируем маршрут /wp-json/custom/v1/orders-for-export. Три параметра: фильтр по дате, статусу и количество записей на страницу. Параметр permission_callback отвечает за авторизацию — об этом поговорим отдельно.
Callback-функция: формируем ответ
Сама функция-обработчик собирает данные из базы и возвращает структурированный JSON.
function handle_orders_export($request) {
$after = $request->get_param('after');
$status = $request->get_param('status');
$per_page = $request->get_param('per_page');
$args = [
'limit' => $per_page,
'type' => 'shop_order',
'orderby'=> 'date',
'order' => 'ASC',
];
if ($status) {
$args['status'] = $status;
}
if ($after) {
$args['date_created'] = '>' . $after . ' 00:00:00';
}
$orders = wc_get_orders($args);
$result = [];
foreach ($orders as $order) {
$items = [];
foreach ($order->get_items() as $item) {
$product = $item->get_product();
$items[] = [
'sku' => $product ? $product->get_sku() : '',
'name' => $item->get_name(),
'quantity' => $item->get_quantity(),
'price' => $item->get_total(),
];
}
$result[] = [
'order_id' => $order->get_id(),
'date_created' => $order->get_date_created()->format('Y-m-d H:i:s'),
'status' => $order->get_status(),
'total' => $order->get_total(),
'currency' => $order->get_currency(),
'payment_method'=> $order->get_payment_method_title(),
'customer_name' => $order->get_billing_first_name() . ' ' . $order->get_billing_last_name(),
'customer_email'=> $order->get_billing_email(),
'customer_phone'=> $order->get_billing_phone(),
'shipping_city' => $order->get_shipping_city(),
'items' => $items,
'meta' => [
'exported' => $order->get_meta('_exported_to_erp'),
],
];
}
return new WP_REST_Response($result, 200);
}
Обратите внимание на поле meta.exported. Это хитрый приём: с помощью произвольного метаполя _exported_to_erp можно отслеживать, какие заказы уже были переданы. После успешной выгрузки внешняя система (или скрипт на стороне WordPress) проставляет это поле, и повторная выгрузка не дублирует данные.
Авторизация: как защитить кастомный API
Открытый endpoint, который отдаёт данные о заказах — это подарок для злоумышленника. Безопасность нельзя оставлять на потом, она проектируется с самого начала.
Есть несколько подходов к авторизации кастомных точек доступа REST API.
API-ключи через заголовок
Самый простой вариант. Внешняя система передаёт секретный ключ в заголовке запроса. Сервер сверяет его с хранимым значением.
function verify_api_access($request) {
$token = $request->get_header('X-API-Key');
if (!$token) {
return new WP_Error(
'rest_forbidden',
'API ключ не передан',
['status' => 401]
);
}
$valid_token = get_option('custom_api_export_key');
if (!$valid_token || !hash_equals($valid_token, $token)) {
return new WP_Error(
'rest_forbidden',
'Неверный API ключ',
['status' => 403]
);
}
return true;
}
Ключ генерируется один раз и передаётся администратору внешней системы. Хранится в wp_options, при желании можно зашифровать.
OAuth или JWT
Для более сложных сценариев — например, когда внешних систем несколько и каждой нужен свой уровень доступа — подойдут JWT-токены (плагин JWT Authentication for WP REST API) или полная реализация OAuth 2.0. Это выходит за рамки базовой настройки, но для enterprise-интеграций часто необходимо.
Дополнительные меры
- Ограничьте доступ по IP — если внешняя система работает с фиксированного адреса, добавьте проверку в
permission_callback. - Используйте HTTPS. Всегда. Без вариантов. Передача данных о заказах по HTTP — это не просто плохая практика, это нарушение базовых требований информационной безопасности.
- Логируйте обращения к API. Даже простая запись в отдельную таблицу или лог-файл с IP, timestamp и параметрами запроса потом сэкономит часы при расследовании инцидентов.
Оптимизация производительности при массовой выгрузке
Когда заказов становится много — тысячи, десятки тысяч — стандартные подходы начинают буксовать. wc_get_orders() загружает объекты в память, инициализирует каждый как полноценный объект WC_Order. Для 100 заказов это незаметно, для 5000 — может привести к исчерпанию памяти или длительному времени ответа.
Прямой SQL-запрос
Для критичных по производительности сценариев имеет смысл обойти ORM WooCommerce и обратиться к базе напрямую.
function handle_orders_export_raw($request) {
global $wpdb;
$after = $request->get_param('after');
$per_page = $request->get_param('per_page');
$sql = $wpdb->prepare("
SELECT
p.ID as order_id,
p.post_date as date_created,
pm_status.meta_value as status,
pm_total.meta_value as total,
pm_currency.meta_value as currency,
pm_fname.meta_value as first_name,
pm_lname.meta_value as last_name,
pm_email.meta_value as email,
pm_phone.meta_value as phone
FROM {$wpdb->posts} p
LEFT JOIN {$wpdb->postmeta} pm_status
ON p.ID = pm_status.post_id AND pm_status.meta_key = '_wc_order_status'
LEFT JOIN {$wpdb->postmeta} pm_total
ON p.ID = pm_total.post_id AND pm_total.meta_key = '_order_total'
LEFT JOIN {$wpdb->postmeta} pm_currency
ON p.ID = pm_currency.post_id AND pm_currency.meta_key = '_order_currency'
LEFT JOIN {$wpdb->postmeta} pm_fname
ON p.ID = pm_fname.post_id AND pm_fname.meta_key = '_billing_first_name'
LEFT JOIN {$wpdb->postmeta} pm_lname
ON p.ID = pm_lname.post_id AND pm_lname.meta_key = '_billing_last_name'
LEFT JOIN {$wpdb->postmeta} pm_email
ON p.ID = pm_email.post_id AND pm_email.meta_key = '_billing_email'
LEFT JOIN {$wpdb->postmeta} pm_phone
ON p.ID = pm_phone.post_id AND pm_phone.meta_key = '_billing_phone'
WHERE p.post_type = 'shop_order'
AND p.post_date > %s
ORDER BY p.post_date ASC
LIMIT %d
", $after, $per_page);
$results = $wpdb->get_results($sql, ARRAY_A);
return new WP_REST_Response($results, 200);
}
Этот запрос выполняется в разы быстрее, чем цикл через wc_get_orders(), особенно на больших объёмах. Правда, он жёстко привязан к структуре метаданных WooCommerce — если в будущих версиях что-то изменится, придётся адаптировать SQL. Но для внутренних интеграций это приемлемый компромисс.
Кэширование и батчинг
Если внешняя система запрашивает данные часто, имеет смысл кэшировать результат. Можно сохранять сформированный JSON-файл на диске и обновлять его по cron-заданию, а endpoint просто отдаёт готовый файл. Для высоконагруженных магазинов это снимает нагрузку с базы данных.
Б
Добавить комментарий