Price Drop Subscription Configuration in 1C-Bitrix
A customer looks at an expensive product and doesn't buy. A "Notify me when the price drops" button gives you a chance to bring them back at exactly the moment the price becomes acceptable. This works better than any mass mailing, because the user has already expressed interest in a specific product.
Data structure
HL block PriceSubscription for storing subscriptions:
b_uts_price_subscription
├── ID
├── UF_USER_ID — user ID (0 = guest)
├── UF_EMAIL — notification email
├── UF_PRODUCT_ID — product ID (b_iblock_element.ID)
├── UF_TARGET_PRICE — desired price (0 = any drop)
├── UF_CURRENT_PRICE — price at the time of subscription
├── UF_ACTIVE — whether the subscription is active
├── UF_NOTIFIED — whether a notification was sent
└── UF_DATE_CREATE — creation date
Subscription form
On the product card, the button appears next to the price:
// In the card template, below the price block
<?php if (!$arResult['CATALOG_ITEM']['CAN_BUY']): // only if the item cannot be purchased now ?>
<?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 me when price drops' ?>
</button>
<?php else: ?>
For authenticated users — AJAX subscription. For guests — display 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; // passed from PHP
if (!email) {
// Show modal with email field
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 subscription handler
// /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]);
Subscription management service
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 {
// Check for duplicate
if (self::isSubscribed($userId, $productId, $email)) {
return true; // already subscribed
}
$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']]);
}
}
Price drop check agent
The agent runs once an hour, finds products with a dropped price, and sends notifications:
namespace Local\Pricing;
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, '', ' '),
]);
}
}
Implementation timelines
| Configuration | Timeline |
|---|---|
| Subscription (button + AJAX + HL block) | 2–3 days |
| + price check agent + email notification | +2 days |
| + target price, subscription account section | +2–3 days |







