Web

Bitrix sale.order.ajax: Persist API Recalculated Prices

Learn how to ensure external API recalculated prices persist to orders in custom bitrix:sale.order.ajax templates using OnSaleOrderBeforeSaved, OnSaleComponentOrderCreated events, RECREATE_ORDER flag, and server-side handlers for D7 Order/Basket.

1 answer 1 view

How can I ensure recalculated product prices from an external API are used when placing an order in a custom bitrix:sale.order.ajax template?

I have a non-standard template of the bitrix:sale.order.ajax component. In result_modifier.php I call an external API, sending the entire cart and promo code; the API returns a recalculated cart and I update the displayed prices. However, when I click the “Place Order” button the order is created with the standard/original prices. Where am I going wrong and how should I apply the recalculated prices so they persist to the order? Which component files, POST fields, or Bitrix events/hooks (e.g., modifying basket items before order creation) should I use to ensure the updated prices are saved?

You’re updating display prices in result_modifier.php, but sale.order.ajax builds and saves a D7 Order/Basket on the server — so front-end-only changes won’t persist. To make external-API recalculated product prices stick you must apply them server-side to the D7 Order->Basket (either when the component builds the order or just before save) using component events (OnSaleComponentOrderCreated, OnSaleOrderBeforeSaved or OnSaleBasketItemBeforeSaved), mark items as custom-priced and pass results from template to server via a secure token/session (don’t trust raw client POST price fields).


Contents


Why result_modifier.php changes don’t persist

result_modifier.php runs when the component renders the page and only changes $arResult for display. The final order creation path uses the D7 Order/Basket objects on the server, not the template’s displayed values; the save operation reconstructs basket data from server-side state (basket DB / product catalog) and applies catalog/discount rules. So changing prices only for the UI will not affect the Order stored in the database. The official docs note the new sale.order.ajax component uses the D7 Order API — so server-side modification points are required https://dev.1c-bitrix.ru/api_help/sale/events/events_components.php.


How bitrix:sale.order.ajax builds the D7 Order and Basket


Where to apply recalculated prices — recommended patterns

Two practical, secure patterns:

  1. Server-side recalculation (preferred)
  • Call the external pricing API from the server (component event or custom AJAX endpoint) and write the returned prices directly into the Order->Basket before the component finishes or before saving. This avoids trusting the client and keeps the authoritative prices server-side.
  1. Token/session bridge (when the component/template calls API)
  • If you must call the API in result_modifier.php (or client-side), persist that API result on the server (session, cache, DB) and put a short-lived token into the checkout form (hidden input). On order save read the token in a server event and apply stored prices to the Order->Basket. Don’t accept raw client prices.

Why not POST raw prices? The component normally reconstructs the basket on the server and ignores client-sent price fields (and even if it didn’t, client data can be tampered). So pass a server-validated reference (token) or run API calls server-side.


Bitrix events and hooks to use (quick reference)

Use these D7 events/hooks (official docs and examples):

  • OnSaleComponentOrderCreated — fired while the component constructs the Order object; good place to alter Order before display or to store server-side recalculation results. Example usage in community posts: https://tichiy.ru/wiki/rabota-s-sale-order-ajax-v-bitriks/.

  • OnSaleOrderBeforeSaved — fires before the Order is saved to DB; get the Order object via $event->getParameter(“ENTITY”) and change basket item prices here so the saved order uses your values: https://dev.1c-bitrix.ru/api_d7/bitrix/sale/events/order_saved.php (the page also shows OnSaleOrderSaved usage).

  • OnSaleBasketItemBeforeSaved / OnSaleBasketBeforeSaved — per-item or whole-basket interception before basket save; useful when you want to guarantee price persistence at the basket layer: https://dev.1c-bitrix.ru/api_d7/bitrix/sale/events/basket_saved.php.

  • OnSaleOrderSaved / OnSaleBasketSaved — post-save hooks for logging, notifying, or cleanup only (they run after save).

The component docs specifically mention using these D7 events for modifying the order https://dev.1c-bitrix.ru/api_d7/bitrix/sale/events/event_sale_order_ajax.php.


Code recipe — token/session approach (recommended if the API call happens in the template)

Flow: result_modifier.php (or your template) calls API → stores results server-side keyed by token → embeds token in a hidden input → OnSaleOrderBeforeSaved reads token and applies prices.

  1. result_modifier.php (store API result and expose token)
php
// result_modifier.php (server-side)
$recalc = callExternalApiWithBasketAndPromo($this->arResult['BASKET'], $promoCode); // your function
$token = bin2hex(random_bytes(16));
$_SESSION['external_prices_'.$token] = [
 'created' => time(),
 'basket' => $recalc['items'], // map by PRODUCT_ID or CUSTOM key
];
// expose token to template
$this->arResult['EXTERNAL_PRICES_TOKEN'] = $token;
  1. Template (add hidden input inside the sale.order.ajax form)
html
<input type="hidden" name="EXTERNAL_PRICES_TOKEN" value="<?=$arResult['EXTERNAL_PRICES_TOKEN']?>">
  1. init.php — event handler to apply prices before save
php
use Bitrix\Main\Event;
use Bitrix\Main\EventManager;
use Bitrix\Main\EventResult;
use Bitrix\Main\Context;

EventManager::getInstance()->addEventHandler('sale', 'OnSaleOrderBeforeSaved', ['MyHandlers','onSaleOrderBeforeSaved']);

class MyHandlers {
 public static function onSaleOrderBeforeSaved(Event $event) {
 $order = $event->getParameter('ENTITY'); // \Bitrix\Sale\Order
 $request = Context::getCurrent()->getRequest();
 $token = $request->get('EXTERNAL_PRICES_TOKEN');
 if (!$token) return new EventResult(EventResult::SUCCESS);

 $data = $_SESSION['external_prices_'.$token] ?? null;
 if (!$data || (time() - ($data['created'] ?? 0) > 3600)) {
 // token expired or missing — ignore or log
 return new EventResult(EventResult::SUCCESS);
 }

 $basket = $order->getBasket();
 foreach ($basket as $item) {
 $pid = $item->getProductId();
 if (isset($data['basket'][$pid])) {
 $price = (float)$data['basket'][$pid]['PRICE'];
 $item->setField('PRICE', $price);
 $item->setField('BASE_PRICE', $price);
 $item->setField('CUSTOM_PRICE', 'Y'); // prevent automatic re-pricing
 }
 }

 return new EventResult(EventResult::SUCCESS);
 }
}

Notes:

  • Map recalculated rows by a stable key (PRODUCT_ID or basket code). If your recalculation depends on quantity or promo, include those in the stored snapshot and validate them against the current basket before applying.
  • Use a short TTL and per-token validation to avoid reusing stale results.

Inline references: doc pages show event usage and that the component uses D7 Order objects https://dev.1c-bitrix.ru/api_help/sale/events/events_components.php.


Code recipe — server-side recalculation (recommended overall)

If possible, move the external-API call to a server-side event so the Order is modified before display/save. Two places:

A) OnSaleComponentOrderCreated — change Order for display and optionally ask the component to recalc totals so the UI shows the same values:

php
EventManager::getInstance()->addEventHandler('sale', 'OnSaleComponentOrderCreated', 'onSaleComponentOrderCreated');

function onSaleComponentOrderCreated($order, &$arUserResult, $request, &$arParams, &$arResult /*, ... */) {
 // $order is \Bitrix\Sale\Order
 $basket = $order->getBasket();
 $itemsForApi = [];
 foreach ($basket as $item) {
 $itemsForApi[] = ['PRODUCT_ID' => $item->getProductId(), 'QUANTITY' => $item->getQuantity()];
 }
 $recalc = callExternalApi($itemsForApi, $request->get('promo')); // server-side call
 // apply returned prices
 foreach ($basket as $item) {
 $pid = $item->getProductId();
 if (isset($recalc[$pid])) {
 $p = (float)$recalc[$pid]['PRICE'];
 $item->setField('PRICE', $p);
 $item->setField('BASE_PRICE', $p);
 $item->setField('CUSTOM_PRICE', 'Y');
 }
 }
 // if needed, ask component/UI to recalc using RECREATE_ORDER (see docs)
 // $arUserResult['RECREATE_ORDER'] = true; // check component event docs for exact flag usage
}

B) OnSaleOrderBeforeSaved — last chance before DB save (same pattern as token handler but you can call API again here if you trust server call latency).

Which to pick?

  • If your external API must see exact basket/promo state at save time, use OnSaleOrderBeforeSaved to re-run (or validate) and apply final prices.
  • If you prefer the UI to show final prices immediately, use OnSaleComponentOrderCreated and request a component recalculation so the displayed totals match.

See community examples of using OnSaleComponentOrderCreated to access Order and basket price https://tichiy.ru/wiki/rabota-s-sale-order-ajax-v-bitriks/.


RECREATE_ORDER and recalculation interaction

Bitrix added a RECREATE_ORDER flag so component events can request a recalculation of the order after your event changes (useful when you modify the Order during the component render). The official event page documents that behavior for sale.order.ajax — consult it to set the flag from your handler so UI totals update after you change prices https://dev.1c-bitrix.ru/api_d7/bitrix/sale/events/event_sale_order_ajax.php.

If you apply final prices only in OnSaleOrderBeforeSaved then RECREATE_ORDER is not required for persistence — it’s only needed when you need the component to re-run calculations and update the UI before the user clicks Place Order.


Testing and debugging checklist

  • Confirm token presence: render checkout, inspect page source and verify a hidden EXTERNAL_PRICES_TOKEN is present and matches a server-side entry.
  • Place an order and log inside the handler:
  • Write $order->getBasket()->getPrice() and each item price inside OnSaleOrderBeforeSaved and OnSaleOrderSaved to a file or debug log.
  • Example logging: \Bitrix\Main\Diag\Debug::writeToFile($order->getBasket()->getPrice(), ‘’, ‘/upload/logs/prices.log’);
  • Verify saved order in admin: open the order in Bitrix admin and check item prices and order total.
  • Test tampering: try submitting a modified token or no token — ensure the server ignores/invalidates it.
  • Test discounts/taxes: create scenarios with discounts to ensure setting CUSTOM_PRICE prevents unwanted re-pricing; verify tax calculations still work.
  • Multi-tab/concurrency: open two tabs, change cart in one, place order in other — confirm token validation or snapshot check prevents mismatch.

Edge cases and recommendations

  • Discounts and catalog rules may recalc prices; mark items with CUSTOM_PRICE to avoid automatic re-pricing.
  • Basket item keys: prefer stable mapping by PRODUCT_ID and quantity or by basket item code (if available) when applying stored prices.
  • Session vs DB: session is easiest but can collide across tabs; for robust flows store per-token data in cache/DB with short TTL.
  • Security: never accept client-submitted numeric prices as authoritative. Use tokens and server-side validation or call API on the server at save time.
  • Performance: calling external API in OnSaleOrderBeforeSaved will add latency to order creation. If that’s a problem, precompute on component render (server-side) and persist results with token.
  • If you customize component.php instead of using events, ensure you’re modifying the actual Order->Basket used to save; but prefer events to avoid forking core component logic.

Sources


Conclusion

You’re almost there — the gap is that result_modifier.php only changes displayed values while the D7 Order/Basket used at save remains unchanged. Fix it by applying the external-API recalculated prices on the server (best: in OnSaleComponentOrderCreated or OnSaleOrderBeforeSaved / OnSaleBasketItemBeforeSaved), mark items as custom-priced and pass API results via a server-side store + short-lived token (instead of trusting client POST price fields). Use RECREATE_ORDER when you need the component to re-run UI calculations after your change. If you want, I can produce a ready-to-drop init.php handler and a small template change for your exact basket schema (product IDs vs basket codes) — tell me which storage ($_SESSION, cache, DB) and the key format you prefer.

Authors
Verified by moderation
Moderation
Bitrix sale.order.ajax: Persist API Recalculated Prices