Корзина в headless Bitrix: пять проблем, которые нас удивили
Каталог мы отдали в Next.js за девять дней. Корзину интегрировали за месяц. Оформление заказа — ещё семь недель.
Потом я посмотрел на дату первого коммита и дату первой успешной продажи через headless-фронт. Разница — два с половиной месяца на то, что на первый взгляд выглядело как «добавить товар, посчитать итог, принять оплату».
Bitrix.Sale — это не API магазина. Это маленькая CRM с собственной логикой, которую REST отдаёт частично и не всегда предсказуемо.
Почему каталог легко, а корзина — нет
Каталог в headless работает предсказуемо: запрашиваешь catalog.product.list, получаешь данные, рендеришь в Next.js. Элемент инфоблока не знает, кто смотрит. Он просто существует.
Корзина устроена иначе. Она привязана к пользователю, к сессии, к истории действий. За ней стоит Bitrix\Sale\Order — объект с состоянием, который живёт в серверном PHP-контексте. REST API только смотрит в это окошко, доступ к объекту напрямую не даётся.
Когда Next.js просит Bitrix добавить товар в корзину, Bitrix должен знать, чья это корзина. Вот тут и начинается.
Проблема 1: сессии в cross-domain архитектуре
Bitrix хранит корзину в серверной сессии. Сессия привязана к cookie PHPSESSID. Когда фронт на shop.example.com делает запрос к backend.example.com/api/ — это cross-domain. Браузер не отправляет cookie без явных заголовков SameSite=None; Secure.
Мы потратили три дня на диагностику ситуации, когда корзина работала в одном браузере и не работала в другом. Оказалось — Safari блокировал сторонние cookies по умолчанию. Корзина исчезала.
Решение простое, но требует понимания: REST-запросы должны идти с credentials: 'include' на фронте, а на бэке — правильный CORS с Access-Control-Allow-Credentials: true и явным списком доменов. Wildcard * с credentials не работает.
Дополнительная сложность: если пользователь авторизован, нужно прокидывать не только сессионный cookie, но и BX_SESSID для CSRF-защиты при мутирующих операциях. Эта связка нигде в официальной документации не объяснена как система.
Проблема 2: sale.order.add и то, чего он не умеет
sale.order.add — эндпоинт для создания заказа через REST. На бумаге он делает всё. На практике он не умеет:
- применять купоны через параметр напрямую (нужен отдельный вызов
sale.basket.addItemс coupon_code, и то не всегда) - вычислять финальную цену с учётом персональных скидок пользователя до создания заказа
- работать с нестандартными обработчиками заказа, добавленными через PHP-события
Мы обнаружили это на проекте, где у клиента была накопительная система скидок, написанная на OnSaleOrderBeforeSaved. REST-эндпоинт эту логику обходил. Заказы создавались с ценой без скидки. Три дня отладки, пока не нашли место. (В предыдущей статье про сюрпризы Bitrix REST API мы разбирали offset-pagination и OAuth — это другой класс проблем, но корень тот же: REST API не раскрывает весь внутренний контракт PHP-слоя.)
Если у вас нестандартная бизнес-логика в Sale-модуле — проверьте её совместимость с REST до начала headless-проекта. Не после.
Проблема 3: купоны и персональные скидки
В монолитном Bitrix это работает через серверный контекст: пользователь вошёл, система знает его группу, и скидки применяются при оформлении сами.
В headless REST API ситуация другая. Персональная скидка применяется, если REST-запрос приходит от авторизованного пользователя с корректной сессией. Но «корректная сессия» в REST-контексте — это не то же самое, что авторизация на сайте.
На практике: если пользователь вошёл через кастомный механизм авторизации (а у большинства наших клиентов он именно кастомный), REST API может «не видеть» группы пользователя и не применять скидки.
Мы в итоге вынесли расчёт финальной цены на PHP-прокси-эндпоинт, который живёт на том же сервере что и Bitrix, имеет прямой доступ к PHP API и возвращает уже посчитанную цену. Это не идеально с точки зрения архитектуры headless, но это работает.
Проблема 4: службы доставки и их виджеты
СДЭК, Почта России, PickPoint — у всех есть виджеты для выбора пункта выдачи. Все виджеты написаны в расчёте на то, что они встраиваются в PHP-страницу Bitrix.
Виджет СДЭК ожидает серверный рендеринг и наличие определённых PHP-переменных в контексте страницы. В Next.js этого контекста нет. Виджет просто не инициализируется.
Решение: встраивать виджеты через iframe на выделенные PHP-страницы Bitrix, которые остаются в монолитной части, и передавать выбранный пункт через postMessage в Next.js. Некрасиво. Зато работает.
Проблема 5: платёжные шлюзы
Большинство платёжных шлюзов, подключённых к Bitrix, работают по redirect-flow: пользователь нажимает «Оплатить», Bitrix формирует ссылку и редиректит на страницу шлюза, шлюз после оплаты редиректит обратно на Bitrix-страницу подтверждения.
В headless-архитектуре обратный редирект попадает на PHP-страницу Bitrix, а не в Next.js. Это ломает UX: пользователь оказывается на старом шаблоне Bitrix после оплаты.
Решений два:
- Оставить страницу подтверждения оплаты на Bitrix-шаблоне (мы так и сделали — это быстрее)
- Настроить кастомный redirect с Bitrix-страницы на Next.js с передачей order ID и статуса
Шлюзов с полноценным API-flow (без редиректа) в экосистеме Bitrix немного. ЮKassa умеет — но требует отдельной реализации обработчика webhook.
Что мы оставили на PHP и почему
По итогу нашего проекта checkout живёт в гибридной модели:
- Корзина (добавление/удаление) — через REST API с cookie forwarding
- Расчёт итогов и скидок — PHP-прокси на том же сервере
- Страница оформления заказа — Next.js форма с REST для создания заказа
- Страница подтверждения оплаты — Bitrix PHP шаблон
Это не чистый headless. Но это работает, приносит деньги, и я не буду делать вид, что было иначе.
Headless Bitrix — это не замена всего PHP одним REST API. Каталог и рендеринг уходят в Next.js. Бизнес-логика Sale-модуля остаётся на PHP. Задача инженера — провести эту границу правильно. Не стереть её. Подробнее о том, что именно мы оставляем на стороне Bitrix и почему — в разборе архитектурных границ.
Мы уложились. Просто дольше, чем думали в начале.