Developing a Subscription System for 1C-Bitrix
A subscription system is a model where a client pays regularly (monthly, quarterly, annually) and gains access to a service or goods. 1C-Bitrix does not have a built-in recurring payment mechanism — it must be built on top of the sale, catalog modules and custom code.
Types of Subscriptions in Bitrix Context
Before designing, it is important to define the business model by subscription type:
| Type | Description | Technical Implementation |
|---|---|---|
| Content Access | Paid section, restricted materials | User groups + access restrictions |
| Product Subscription | Regular goods delivery | Automatic order creation |
| Service Subscription | SaaS, license, support | Account status + auto-renewal |
All three types share common features: periodic payment and access status management.
Data Storage Architecture
The core of the system is the subscriptions table. Created via the Bitrix ORM (inheritance from \Bitrix\Main\ORM\Data\DataManager):
class SubscriptionTable extends DataManager
{
public static function getTableName(): string
{
return 'b_local_subscription';
}
public static function getMap(): array
{
return [
new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
new IntegerField('USER_ID', ['required' => true]),
new IntegerField('PLAN_ID', ['required' => true]),
new EnumField('STATUS', ['values' => ['TRIAL', 'ACTIVE', 'PAST_DUE', 'CANCELLED', 'EXPIRED']]),
new DatetimeField('CURRENT_PERIOD_START'),
new DatetimeField('CURRENT_PERIOD_END'),
new DatetimeField('TRIAL_END'),
new StringField('PAYMENT_TOKEN'), // Recurring payment token
new IntegerField('PAY_SYSTEM_ID'),
new StringField('CANCEL_REASON'),
new DatetimeField('CANCELLED_AT'),
new DatetimeField('CREATED_AT'),
];
}
}
Subscription plans table:
class SubscriptionPlanTable extends DataManager
{
// ID, NAME, PRICE, CURRENCY, PERIOD_DAYS, TRIAL_DAYS,
// IBLOCK_SECTION_IDS (available sections), FEATURES (JSON)
}
Access Management by Subscription
For "content" type subscriptions, access is realized through Bitrix user groups. Each plan corresponds to a group (b_group). Upon subscription activation — add the user to the group:
CUser::SetUserGroup($userId, array_merge(
CUser::GetUserGroup($userId),
[$plan->getGroupId()]
));
Upon expiration or cancellation — remove from the group. Access to sections is restricted by infoblock or component permissions.
Recurring Payments
This is the most complex part. Recurring payments require the payment system to store the payment method (card) and perform automatic charges on demand. Supported systems: YooKassa (Yandex.Kassa), CloudPayments, Robokassa (recurring), Stripe (for international clients).
Workflow:
-
First payment: regular payment through the payment system API. In response —
payment_method_idor saved method token. -
Save token: record in
SubscriptionTable.PAYMENT_TOKEN. -
Auto-charging: when
CURRENT_PERIOD_ENDis reached — call the payment system API with the token:
// Example for YooKassa
$payment = new \YooKassa\Client();
$payment->setAuth($shopId, $secretKey);
$response = $payment->createPayment([
'amount' => ['value' => $plan->getPrice(), 'currency' => 'RUB'],
'payment_method_id' => $subscription->getPaymentToken(),
'capture' => true,
'description' => 'Subscription ' . $plan->getName() . ' #' . $subscription->getId(),
]);
-
Handle result: success → update
CURRENT_PERIOD_START,CURRENT_PERIOD_END, save new statusACTIVE. Failure → statusPAST_DUE, notify client, retry in 24/48 hours.
Trial Period
When creating a subscription with a trial:
$trialEnd = (new DateTime())->modify('+' . $plan->getTrialDays() . ' days');
SubscriptionTable::add([
'USER_ID' => $userId,
'PLAN_ID' => $planId,
'STATUS' => 'TRIAL',
'TRIAL_END' => $trialEnd,
'CURRENT_PERIOD_END' => $trialEnd,
]);
An agent 1 day before trial end — notify the user. On the last day — attempt the first charge. If no card is attached — change status to EXPIRED.
Subscription Processing Agent
Daily cron script:
// Find subscriptions requiring renewal
$expiring = SubscriptionTable::getList([
'filter' => [
'STATUS' => ['ACTIVE', 'PAST_DUE'],
'<=CURRENT_PERIOD_END' => new DateTime(),
]
])->fetchAll();
foreach ($expiring as $sub) {
try {
$result = chargeSubscription($sub);
if ($result->isSuccess()) {
SubscriptionTable::update($sub['ID'], [
'STATUS' => 'ACTIVE',
'CURRENT_PERIOD_START' => new DateTime(),
'CURRENT_PERIOD_END' => (new DateTime())->modify('+' . $planPeriodDays . ' days'),
]);
} else {
handlePaymentFailure($sub);
}
} catch (\Exception $e) {
logError($e, $sub);
}
}
Subscriber Personal Account
Minimal functionality for the /personal/subscription/ page:
- Current plan and status.
- Next billing date and amount.
- Payment history.
- Cancel button (with optional reason prompt).
- Plan changes (upgrade/downgrade).
- Payment method update.
Upon cancellation: STATUS = 'CANCELLED', CANCELLED_AT = now(). Access is preserved until CURRENT_PERIOD_END — the client paid for the period and uses what they paid for.
Notifications
Required email events:
- SUBSCRIPTION_CREATED — subscription confirmation.
- SUBSCRIPTION_PAYMENT_SUCCESS — successful charge, receipt.
- SUBSCRIPTION_PAYMENT_FAILED — charge error, update card.
- SUBSCRIPTION_TRIAL_ENDING — 1-3 days before trial ends.
- SUBSCRIPTION_CANCELLED — cancellation confirmation.
- SUBSCRIPTION_EXPIRED — access closed.
Development Timeline
| Option | Scope | Timeline |
|---|---|---|
| Without Recurring | Subscription with manual payment, access management | 5-7 days |
| With Recurring Payments | Auto-charging via YooKassa or CloudPayments | 10-14 days |
| Full Platform | Multiple plans, trial, personal account, analytics | 15-20 days |







