Setting Up Fund Holding on 1C-Bitrix
The customer paid for the order — the money is debited, but the order is not yet confirmed. Two days later, it turns out there is no product in stock. A refund — that's another few days. Fund holding (two-stage payment) solves this problem: the bank reserves the amount on the customer's card, and the actual debit occurs only after confirming the product is available.
Two-Stage Payment: How It Works
Stage 1 — Hold (authorization): the store sends a request to the payment system to reserve the amount. The money is not debited, but blocked on the card. The acquirer bank issues a pre_auth_code or similar authorization identifier.
Stage 2 — Confirmation (capture): the store confirms the debit when the product is ready to ship. The money goes to the store's account. Or — cancellation (void/cancel): if there is no product, the reserve is removed without fees.
Bitrix implements this via the sale module, \Bitrix\Sale\PaySystem\Manager class, and specific payment system handlers.
Two-Stage Payment Support in the sale Module
Not all payment systems in Bitrix support two-stage payment. Built-in support is available from:
- Sberbank (via
SberPaymenthandler) - Tinkoff (
TinkoffPayment) - YooKassa — YooKassa (
YandexMoney) - Alfa-Bank (
AlfaPayment)
Payment system configuration in the admin panel: "Store" → "Payment" → select payment system → "Settings" tab → "Two-Stage Payment" or "Holding Mode" parameter.
Holding Configuration in Database Table
Payment system parameters are stored in tables b_sale_pay_system and b_sale_pay_system_handler:
-- Find payment system ID
SELECT ID, NAME, HANDLER_TYPE FROM b_sale_pay_system WHERE ACTIVE = 'Y';
-- Parameters of a specific payment system
SELECT PS_ID, CODE, VALUE FROM b_sale_pay_system_params WHERE PS_ID = 5;
Programmatic Work with Holding
Payment statuses in b_sale_payment:
-
N— not paid -
Y— paid (money debited)
Custom holding statuses are usually stored in additional fields or via b_sale_order_props.
Confirming payment (capture) via API:
use Bitrix\Sale;
// Get order
$order = Sale\Order::load($orderId);
// Get payment
$paymentCollection = $order->getPaymentCollection();
foreach ($paymentCollection as $payment) {
if (!$payment->isPaid()) {
// Confirm held payment
$result = Sale\PaySystem\Manager::confirm($payment);
if (!$result->isSuccess()) {
// Handle error
$errors = $result->getErrorMessages();
}
}
}
Canceling hold:
foreach ($paymentCollection as $payment) {
$result = Sale\PaySystem\Manager::cancel($payment);
if ($result->isSuccess()) {
// Zero out payment amount
$payment->setField('SUM', 0);
}
}
$order->save();
Event Handler for Order Confirmation
Automatic confirmation (capture) when order is moved to "Confirmed" status:
// /bitrix/php_interface/init.php
use Bitrix\Main\EventManager;
EventManager::getInstance()->addEventHandler(
'sale',
'OnSaleStatusOrderChange',
function(\Bitrix\Main\Event $event) {
$order = $event->getParameter('ENTITY');
$value = $event->getParameter('VALUE');
// "Confirmed" status — confirm hold
if ($value === 'C') { // ID of your confirmation status
$paymentCollection = $order->getPaymentCollection();
foreach ($paymentCollection as $payment) {
if (!$payment->isPaid()) {
\Bitrix\Sale\PaySystem\Manager::confirm($payment);
}
}
}
// "Cancelled" status — lift hold
if ($value === 'F') {
$paymentCollection = $order->getPaymentCollection();
foreach ($paymentCollection as $payment) {
\Bitrix\Sale\PaySystem\Manager::cancel($payment);
}
}
}
);
Holding Validity Period
Banks limit the holding period. Sberbank — up to 30 days, most foreign acquirers — 7 days. If not confirmed or canceled within this period, the bank automatically removes the reserve.
Set up a Bitrix agent to check for expired holds:
// Agent: ProccessExpiredHolds()
function ProccessExpiredHolds() {
// Find payments in hold status, older than 5 days
$cutoffDate = new \Bitrix\Main\Type\DateTime();
$cutoffDate->add('-5D');
$res = \Bitrix\Sale\Internals\PaymentTable::getList([
'filter' => [
'PAID' => 'N',
'<DATE_BILL' => $cutoffDate,
'!SUM' => 0
],
'select' => ['ID', 'ORDER_ID']
]);
while ($row = $res->fetch()) {
// Send notification to manager about the need to process the order
\CEvent::Send('HOLD_EXPIRING', 's1', [
'ORDER_ID' => $row['ORDER_ID']
]);
}
return 'ProccessExpiredHolds();';
}







