Setting Up Subscription Payments on 1C-Bitrix
The subscription billing model — monthly or annual billing with automatic renewal — requires a separate infrastructure in 1C-Bitrix: a "subscription" entity, periodic charges to a saved card, state management (active, paused, cancelled), and trial period handling. The standard sale module is not designed for this purpose; a custom layer is required.
Subscription Data Model
// Subscription table structure (ORM D7)
class SubscriptionTable extends \Bitrix\Main\Entity\DataManager
{
public static function getTableName(): string
{
return 'b_subscription';
}
public static function getMap(): array
{
return [
'ID' => new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
'USER_ID' => new IntegerField('USER_ID', ['required' => true]),
'PLAN_ID' => new IntegerField('PLAN_ID', ['required' => true]),
'STATUS' => new StringField('STATUS'), // ACTIVE, PAUSED, CANCELLED, TRIAL
'PAYMENT_METHOD' => new StringField('PAYMENT_METHOD'), // card token
'AMOUNT' => new FloatField('AMOUNT'),
'CURRENCY' => new StringField('CURRENCY', ['default_value' => 'RUB']),
'BILLING_CYCLE' => new StringField('BILLING_CYCLE'), // MONTHLY, ANNUAL
'NEXT_BILLING' => new DatetimeField('NEXT_BILLING'),
'TRIAL_END' => new DatetimeField('TRIAL_END'),
'CREATED_AT' => new DatetimeField('CREATED_AT'),
'CANCELLED_AT' => new DatetimeField('CANCELLED_AT'),
];
}
}
Creating a Subscription at Checkout
class SubscriptionService
{
public function create(int $userId, int $planId, bool $trial = false): int
{
$plan = SubscriptionPlanTable::getById($planId)->fetch();
$trialEnd = $trial ? (new \DateTime())->modify('+' . $plan['TRIAL_DAYS'] . ' days') : null;
$nextBill = $trial
? $trialEnd
: (new \DateTime())->modify('+1 ' . strtolower($plan['CYCLE']));
$result = SubscriptionTable::add([
'USER_ID' => $userId,
'PLAN_ID' => $planId,
'STATUS' => $trial ? 'TRIAL' : 'ACTIVE',
'AMOUNT' => $plan['PRICE'],
'BILLING_CYCLE' => $plan['CYCLE'],
'NEXT_BILLING' => \Bitrix\Main\Type\DateTime::createFromPhp($nextBill),
'TRIAL_END' => $trialEnd
? \Bitrix\Main\Type\DateTime::createFromPhp($trialEnd)
: null,
'CREATED_AT' => new \Bitrix\Main\Type\DateTime(),
]);
return $result->getId();
}
// Link a card to the subscription after the first payment
public function attachPaymentMethod(int $subscriptionId, string $paymentMethodId): void
{
SubscriptionTable::update($subscriptionId, [
'PAYMENT_METHOD' => $paymentMethodId,
]);
}
}
Billing Agent: Daily Subscription Processing
class SubscriptionBillingAgent
{
public static function run(): string
{
// Select all subscriptions with NEXT_BILLING <= today
$subscriptions = SubscriptionTable::getList([
'filter' => [
'STATUS' => ['ACTIVE', 'TRIAL'],
'<=NEXT_BILLING' => new \Bitrix\Main\Type\DateTime(),
],
]);
$paymentService = new RecurringPaymentService();
while ($sub = $subscriptions->fetch()) {
if ($sub['STATUS'] === 'TRIAL') {
// Trial period expired — first charge
$sub['STATUS'] = 'ACTIVE';
}
$charged = $paymentService->chargeRecurrent(
$sub['USER_ID'],
$sub['PAYMENT_METHOD'],
$sub['AMOUNT'],
'Subscription renewal #' . $sub['ID']
);
if ($charged) {
// Advance the next billing date
$next = (new \DateTime())->modify(
$sub['BILLING_CYCLE'] === 'ANNUAL' ? '+1 year' : '+1 month'
);
SubscriptionTable::update($sub['ID'], [
'NEXT_BILLING' => \Bitrix\Main\Type\DateTime::createFromPhp($next),
'STATUS' => 'ACTIVE',
]);
} else {
self::handleFailedPayment($sub);
}
}
return '\MyModule\SubscriptionBillingAgent::run();';
}
private static function handleFailedPayment(array $sub): void
{
$attempts = (int)SubscriptionAttemptTable::countFailures($sub['ID'], '-7 days');
if ($attempts >= 3) {
// Cancel the subscription after 3 failures within 7 days
SubscriptionTable::update($sub['ID'], [
'STATUS' => 'CANCELLED',
'CANCELLED_AT' => new \Bitrix\Main\Type\DateTime(),
]);
// Email the buyer
} else {
// Retry in 24 hours — advance NEXT_BILLING by 1 day
SubscriptionTable::update($sub['ID'], [
'NEXT_BILLING' => \Bitrix\Main\Type\DateTime::createFromPhp(
(new \DateTime())->modify('+1 day')
),
]);
}
}
}
Buyer Account Area
Subscription management in the personal account: pause, cancel, change card. Buttons send AJAX requests to the component:
// Pause subscription (stop billing without cancelling)
if ($action === 'pause') {
SubscriptionTable::update($subscriptionId, ['STATUS' => 'PAUSED']);
}
// Resume
if ($action === 'resume') {
$nextBilling = (new \DateTime())->modify('+1 month');
SubscriptionTable::update($subscriptionId, [
'STATUS' => 'ACTIVE',
'NEXT_BILLING' => \Bitrix\Main\Type\DateTime::createFromPhp($nextBilling),
]);
}
Timeline
| Task | Duration |
|---|---|
| Data model + subscription service | 2 days |
| Billing agent + retry logic | 1–2 days |
| Buyer account area (pause, cancel, change card) | 1–2 days |







