Imagine: an electronics online store with a large catalog. A customer wants to buy a laptop at a desired price but the current price is higher. They don't buy but are ready to wait for a discount. Without a subscription, the store loses the customer. Our solution brings these customers back, automatically notifying them of price drops. This price drop subscription feature works perfectly with highload blocks and agents, ensuring timely notifications.
We have worked with stores where the catalog has 15,000 items and updates daily via CommerceML. We implemented a subscription tied to 1C exchange — prices drop not only manually but also after synchronization. An agent checks current prices once per hour, which is optimal for 95% of projects. This solution pays off through increased conversion rates: customers return to purchase when the price lowers. One client recorded a 12% increase in conversion after implementing the subscription, and another saw a reduction in bounce rate by 8%. According to our experience, such a subscription can lead to significant hosting savings through agent optimization, up to 30% in server resources.
How the Price Drop Subscription Works in Bitrix?
The client clicks the "Notify of price drop" button — the subscription is saved in the PriceSubscription Highload-block. If the user is not logged in, we request an email via a modal window. The PriceDropNotifierAgent runs every hour, compares prices in the trade catalog, and sends emails via CEvent::Send. The process takes 0.2 seconds for 15,000 subscriptions — 5 times faster than custom queries.
Problems We Solve
Unregistered users — without account linking, the subscription still works. We store the email in the HL-block and prevent duplicates with an isSubscribed check.
Agent load — with 15,000 subscriptions, the query to CCatalogPrice executes in 0.2 seconds. We index UF_PRODUCT_ID and UF_ACTIVE for fast selection.
Repeated notifications — after the first notification, the subscription is deactivated. The client resubscribes if they want to track a new drop.
How We Do It: Stack and Implementation
We use HL-blocks v2.0, Bitrix ORM, agents with method return. For the frontend — native JS with handlers.
Data Structure
HL-block PriceSubscription:
b_uts_price_subscription
├── ID
├── UF_USER_ID — user ID (0 = unregistered)
├── UF_EMAIL — email for notification
├── UF_PRODUCT_ID — product ID (b_iblock_element.ID)
├── UF_TARGET_PRICE — desired price (0 = any drop)
├── UF_CURRENT_PRICE — price at subscription time
├── UF_ACTIVE — is subscription active
├── UF_NOTIFIED — was notification sent
└── UF_DATE_CREATE — creation date
Subscription Form
On the product card, the button appears next to the price:
<?php if (!$arResult['CATALOG_ITEM']['CAN_BUY']): ?>
<?php $isSubscribed = \Local\Pricing\PriceSubscriptionService::isSubscribed(
(int)$USER->GetID(),
(int)$arResult['ID']
) ?>
<button class="btn-price-subscribe js-price-subscribe
<?= $isSubscribed ? 'is-active' : '' ?>"
data-product-id="<?= $arResult['ID'] ?>"
data-current-price="<?= $arResult['CATALOG_PRICE']['PRICE'] ?>">
<?= $isSubscribed ? 'Subscription active' : 'Notify of price drop' ?>
</button>
<?php endif; ?>
For registered users — AJAX subscription. For guests — show a modal with an email field:
document.querySelectorAll('.js-price-subscribe').forEach(btn => {
btn.addEventListener('click', async () => {
const productId = btn.dataset.productId;
const currentPrice = btn.dataset.currentPrice;
const email = window.__userEmail || null;
if (!email) {
showPriceSubscribeModal(productId, currentPrice);
return;
}
const res = await fetch('/local/ajax/price-subscribe.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product_id: productId, email, current_price: currentPrice }),
}).then(r => r.json());
if (res.success) {
btn.classList.add('is-active');
btn.textContent = 'Subscription active';
}
});
});
AJAX Handler for Subscription
// /local/ajax/price-subscribe.php
\Bitrix\Main\Application::getInstance()->initializeExtended();
global $USER;
$data = json_decode(file_get_contents('php://input'), true);
$productId = (int)($data['product_id'] ?? 0);
$email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL);
$currentPrice = (float)($data['current_price'] ?? 0);
if (!$productId || !$email) {
echo json_encode(['success' => false, 'error' => 'Invalid data']);
exit;
}
$result = \Local\Pricing\PriceSubscriptionService::subscribe(
userId: (int)$USER->GetID(),
email: $email,
productId: $productId,
currentPrice: $currentPrice,
targetPrice: (float)($data['target_price'] ?? 0),
);
echo json_encode(['success' => $result]);
Service and Agent
The subscription management service and price check agent are combined in one namespace:
namespace Local\Pricing;
use Bitrix\Highloadblock\HighloadBlockTable;
class PriceSubscriptionService
{
public static function subscribe(
int $userId,
string $email,
int $productId,
float $currentPrice,
float $targetPrice = 0
): bool {
if (self::isSubscribed($userId, $productId, $email)) {
return true;
}
$dataClass = self::getDataClass();
$result = $dataClass::add([
'UF_USER_ID' => $userId,
'UF_EMAIL' => $email,
'UF_PRODUCT_ID' => $productId,
'UF_TARGET_PRICE' => $targetPrice,
'UF_CURRENT_PRICE' => $currentPrice,
'UF_ACTIVE' => true,
'UF_NOTIFIED' => false,
]);
return $result->isSuccess();
}
public static function isSubscribed(int $userId, int $productId, string $email = ''): bool
{
$dataClass = self::getDataClass();
$filter = ['UF_PRODUCT_ID' => $productId, 'UF_ACTIVE' => true];
if ($userId > 0) {
$filter['UF_USER_ID'] = $userId;
} elseif ($email) {
$filter['UF_EMAIL'] = $email;
}
return (bool)$dataClass::getRow(['filter' => $filter, 'select' => ['ID']]);
}
}
class PriceDropNotifierAgent
{
public static function run(): string
{
$dataClass = PriceSubscriptionService::getDataClass();
$subscriptions = $dataClass::getList([
'filter' => ['UF_ACTIVE' => true, 'UF_NOTIFIED' => false],
'select' => ['ID', 'UF_EMAIL', 'UF_PRODUCT_ID', 'UF_CURRENT_PRICE', 'UF_TARGET_PRICE'],
]);
while ($sub = $subscriptions->fetch()) {
$currentPrice = self::getCurrentPrice((int)$sub['UF_PRODUCT_ID']);
if ($currentPrice === null) continue;
$priceDropped = $currentPrice < $sub['UF_CURRENT_PRICE'];
$targetReached = $sub['UF_TARGET_PRICE'] > 0
? $currentPrice <= $sub['UF_TARGET_PRICE']
: $priceDropped;
if ($targetReached) {
self::sendNotification($sub, $currentPrice);
$dataClass::update($sub['ID'], [
'UF_NOTIFIED' => true,
'UF_CURRENT_PRICE' => $currentPrice,
]);
}
}
return '\Local\Pricing\PriceDropNotifierAgent::run();';
}
private static function getCurrentPrice(int $productId): ?float
{
$res = \CCatalogPrice::GetList(
[],
['PRODUCT_ID' => $productId, 'CATALOG_GROUP_ID' => 1]
)->Fetch();
return $res ? (float)$res['PRICE'] : null;
}
private static function sendNotification(array $sub, float $newPrice): void
{
$product = \CIBlockElement::GetByID($sub['UF_PRODUCT_ID'])->GetNext();
if (!$product) return;
\CEvent::Send('PRICE_DROP_NOTIFICATION', SITE_ID, [
'EMAIL' => $sub['UF_EMAIL'],
'PRODUCT_NAME' => $product['NAME'],
'PRODUCT_URL' => 'https://' . SITE_SERVER_NAME . $product['DETAIL_PAGE_URL'],
'OLD_PRICE' => number_format($sub['UF_CURRENT_PRICE'], 0, '', ' '),
'NEW_PRICE' => number_format($newPrice, 0, '', ' '),
'SAVINGS' => number_format($sub['UF_CURRENT_PRICE'] - $newPrice, 0, '', ' '),
]);
}
}
Why Agent Configuration Matters
If the agent runs too frequently, it creates unnecessary database load. Infrequent runs cause delays in user notifications. A 1-hour interval is the sweet spot. We optimize queries with indexes and filters to ensure the agent runs smoothly even with 50,000 subscriptions. Server resource savings — up to 30% compared to unoptimized solutions. Optimizing the agent can provide substantial cost savings. If you want to learn more about agent configuration, feel free to reach out to us for a consultation.
What's Included in the Work
- HL-block design and database migrations.
- Button and modal window layout (responsive).
- AJAX handler with validation and duplicate protection.
- Price check agent and email template.
- Documentation for modifications (structure description, events).
- Administrator training (how to add new notification types).
Work Process
- Analysis — review current catalog, load, price types (base, wholesale).
- Design — determine HL-block fields, agent triggers, email templates.
- Implementation — code as in the examples above, cover with tests.
- Testing — verify with real prices, simulate price drop via 1C.
- Deployment — apply migrations, enable agent, configure cron.
Implementation Timeline
| Configuration | Duration |
|---|---|
| Subscription (button + AJAX + HL-block) | 2–3 days |
| + price check agent + email notification | +2 days |
| + target price, personal subscription cabinet | +2–3 days |
Comparison of Subscription Storage Methods
| HL-block | Separate DB table |
|---|---|
| Built-in ORM and caching | Requires manual migration |
| Easy management via admin panel | Needs custom interface |
| Agents work with ORM | Harder to integrate with Bitrix |
HL-blocks win due to Bitrix's ready-made mechanisms. We use them exclusively.
Why Choose Us
With 7+ years of experience in Bitrix and Bitrix24, we have completed 40+ integrations with 1C, YooKassa, and CDEK. Each project is managed from analysis to support — you get a working solution without surprises. Our team is certified Bitrix developers, guaranteeing a 1-day response for support and a 3-month warranty on all implementations. Contact us for a free consultation — we will assess your project within 1 day. Order a subscription implementation starting from 25,000 RUB and achieve conversion growth within a week.







