Setting up subscription payments in 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

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