Back to blog

Headless Bitrix cart and checkout: five things we didn't see coming

The catalog took nine days in Next.js. The cart integration took a month. Checkout took another seven weeks.

I looked at the first commit date and the date of the first successful order through the headless frontend. Two and a half months for something that sounds like "add to cart, calculate total, accept payment."

The tutorials show the catalog. They stop before the cart.

The catalog is easy. Checkout is a different beast.

In a headless Bitrix setup, the catalog is stateless: you call catalog.product.list, get data, render it in Next.js. A product has no memory of who's looking at it.

The cart is the opposite. It's tied to a user, a session, a history of actions. Bitrix\Sale\Order is a stateful PHP object that lives server-side. The REST API is a window into that object — not a replacement for it.

When Next.js asks Bitrix to add something to a cart, Bitrix needs to know whose cart. That's where it gets complicated.

Cart sessions: who owns the state in a cross-domain setup?

Bitrix stores the cart in a server-side session. The session is tied to a PHPSESSID cookie. When your frontend at shop.example.com makes requests to backend.example.com/api/ — that's cross-domain. The browser won't send cookies without explicit SameSite=None; Secure headers.

We spent three days diagnosing a situation where the cart worked in Chrome and silently disappeared in Safari. Safari blocks third-party cookies by default. The cart vanished.

The fix itself is straightforward: credentials: 'include' on fetch calls on the frontend, proper CORS with Access-Control-Allow-Credentials: true and an explicit domain allowlist on the backend. Wildcard * doesn't work with credentials — that's a spec requirement, not a Bitrix quirk.

There's one more layer: if the user is authenticated, you also need to forward BX_SESSID for CSRF protection on mutating operations. This auth+session combination isn't explained anywhere in the Bitrix documentation as a complete picture. You piece it together from forum threads.

sale.order.add is not what you think

sale.order.add is the REST endpoint for creating orders. On paper, it covers everything. In practice:

  • It can't apply coupon codes directly as a parameter (you need a separate sale.basket.addItem call with a coupon code, and even that doesn't always work)
  • It can't calculate the final price with personal discounts applied before the order is created
  • It bypasses custom PHP order handlers added via events

We hit the third one on a project with a loyalty discount system built on OnSaleOrderBeforeSaved. The REST endpoint ignored that handler entirely. Orders were created at full price. Three days of debugging before we found it. (The Bitrix REST API headless surprises post covers a different class of issues — pagination, OAuth, rate limits — but the root cause is the same: REST API doesn't expose the full PHP-layer contract.)

If you have custom business logic in the Sale module, check its REST compatibility before starting the headless project. Not after.

Coupons, personal discounts, and the REST API gap

In a Bitrix monolith, discount logic runs automatically in server-side context: the user is authenticated, the system knows their customer group, discounts apply at checkout. It just works.

In a headless REST API setup, personal discounts apply only if the REST request arrives with a valid authenticated session. But "valid authenticated session" in REST context isn't the same thing as being logged into the site.

In practice: if the user authenticated through a custom mechanism (which most of our clients have), the REST API may not see the user's group — and won't apply discounts.

Our solution was a PHP proxy endpoint on the same server as Bitrix, with direct PHP API access, that returns the calculated final price. Not architecturally pure headless. It works and it ships.

Delivery widgets and their server-rendering assumptions

Russian carrier pickup-point widgets (CDEK, PickPoint, Boxberry) are built to be embedded inside PHP pages with full server-side context — specific PHP variables, session state, Bitrix Location module. They don't know how to initialize in a JavaScript frontend.

CDEK's widget, for example, expects certain PHP variables in page scope. In Next.js that scope doesn't exist. The widget silently fails to initialize.

Our fix: embed delivery widgets inside iframes pointing to dedicated PHP pages that stay on the Bitrix monolith, then pass the selected pickup point back to Next.js via postMessage. Ugly. Functional.

Payment gateways: redirect vs API flow

Most payment gateways integrated with Bitrix use a redirect flow: user clicks Pay, Bitrix generates a link, redirects to the gateway, gateway redirects back to a Bitrix confirmation page.

In a headless setup, that return redirect lands on a PHP template — not in Next.js. The user ends up on the old Bitrix UI after payment.

Two options:

  • Keep the payment confirmation page on the Bitrix template (that's what we did — faster to ship)
  • Build a custom redirect from Bitrix to Next.js passing the order ID and status

Gateways with a full API flow (no redirect) are rare in the Bitrix ecosystem. YooKassa can do it — but requires custom webhook handler implementation.

What we kept on the PHP layer (and don't regret)

After this project, our checkout lives in a hybrid model:

  • Cart add/remove: REST API with cookie forwarding
  • Price and discount calculation: PHP proxy on the same server
  • Checkout form: Next.js with REST for order creation
  • Payment confirmation page: Bitrix PHP template

It's not clean headless. But it makes money and I'm not going to pretend otherwise.

The architecture decision of what to keep on Bitrix and what to move is its own conversation — we wrote about it here. The short version: headless doesn't mean PHP disappears. It means PHP does what it's better at.

Two and a half months. We knew it would take longer than a catalog. We didn't know it would be this particular flavor of longer.