Эта статья является второй частью серии статей “Лучшее время для соло предпринимательства” первая часть тут, в которой я описываю свой путь создания и доведения продукта до первой выручки и параллельно пытаюсь создать перечень подходов и инструментов, который может помочь серийно создавать и запускать такие проекты (фреймворк).

Имея такую амбициозную цель и думая о том, как ускорить каждый этап создания продукта, я заметил, что при разработке проекта очень много повторяющихся фич, которые можно шаблонизировать и потом переиспользовать. Есть даже примеры, когда делают из такого “шаблона” бизнес.

Что повторяется:

  1. Лендинг с дефолтными блоками (форма обратной связи, о чём продукт, форма для white list, кнопки “try”, “get started”, блок с партнёрствами, FAQ).

  2. Авторизация/регистрация

  3. Дефолтная страница личного кабинета с sidebar и страницей settings.

  4. Система доступов для уровней подписок

  5. Страницы Terms of Service, Privacy Policy, License Agreement (если нужно).

  6. Блок для сбора фидбека. Фидбек пользователей — это основной ориентир на начальных этапах, он обязательно нужен.

  7. Настройка SMTP для отправки почты.

  8. Инфраструктура сервиса: где хоститься, какую базу данных выбрать, мониторинг/логи.

И наконец — интеграция платежной системы, о которой и пойдёт речь в этой статье. Очень интересный и важный этап, который надо пройти всего один раз, и потом можно спокойно переиспользовать с небольшими доработками.

Как видим, чуть ли не половину всего функционала можно шаблонизировать. Имея такой шаблон, можно, не отвлекаясь на побочный функционал, заниматься разработкой полезного пользователям продукта.


Итак, почти каждый проект нуждается в интеграции платежной системы для получения оплаты за пользу, которую он приносит. Моя цель — запустить проект на международный рынок, и самый очевидный вариант для этого — Stripe. Но я решил попробовать иное средство — платёжную платформу Paddle Billing. И далее расскажу, как интегрировал систему подписок себе в приложение. Я потратил на интеграцию около 20 часов работы, что на данный момент оказалось самой времязатратной задачей. Радует, что это надо пройти только один раз.

Почему Paddle

Это платёжная платформа Merchant of Record (MoR) для софта/SaaS. Проще говоря, вы «продаёте» подписки через Paddle, и формальным продавцом для покупателя выступает Paddle, а не ваша компания. За счёт этого они берут больше ответственности:

  • приём платежей (карты, PayPal, Apple/Google Pay и т. п.);

  • налоги и комплаенс по всему миру (VAT/GST/US sales tax, налоговые номера, инвойсы, кредит-ноты);

  • выставление счетов, чеки/инвойсы, возвраты, чарджбеки;

  • управление подписками (планы, цены, апгрейд/даунгрейд, proration, пауза/отмена);

  • даннинг (ретраи неудачных платежей), антифрод;

  • customer portal для обновления карты/планов;

  • отчётность и payouts вам (вы получаете выплаты от Paddle).

Хоть комиссия выше, чем у Stripe, но, думаю, это того стоит.

Чтобы начать интегрировать платёжку, KYC проходить не обязательно: можно зарегистрироваться и использовать sandbox для интеграции. В sandbox можно полностью воспроизвести весь процесс приёма платежей/управления подписками. А когда у вас будет боевой аккаунт с KYC — заменить переменные окружения.

Моя задача была создать полноценную систему подписок из трёх тиров: free, pro и max — с ценами для оплаты каждый месяц и сразу за год.

Я не буду разжёвывать каждую функцию и каждый передаваемый параметр — на этот счёт имеется довольно неплохая документация. Статью я в целом посвящу базовым действиям по настройке аккаунта, а далее мы пройдёмся детально по основным юзер-сценариям и обозначу нюансы, с которыми я столкнулся.

Базовая настройка

  • Dashboard → Catalog: создайте Product/Plan (Pro и Max) и Price (два для каждого продукта: year и month) — возьмите price_id (например, pri_...). Эти ID вы будете передавать в checkout.

  • Developer Tools → Authentication: создайте client-side token (для фронта, test_.../live_...) и API key для бэка (pdl_sdbx_apikey_01_...). Добавьте в переменные окружения.

  • Developer Tools → Notifications: нажмите New destination и выберите события (events), которые вы хотите получать на свой вебхук. Скопируйте Secret key у Notification и добавьте в переменные окружения.

События, которые обрабатываю я на бэкенде :

subscription.created, subscription.updated, subscription.canceled, subscription.activated, subscription.paused, subscription.past_due, transaction.created, transaction.completed, transaction.updated, transaction.payment_failed

  • Фронтенд: Paddle.js (React/любой SPA). Подключите paddle.js и инициализируйте с client-side token.

  • Checkout → Checkout settings → Default payment link: указываем пока https://localhost/, потом URL вашего сайта.

Чтобы получать events с тестовой среды Paddle на свой https://localhost/, я использую ngrok. Но тогда придётся каждый раз делать апдейт Notification в Developer Tools и менять URL (на бесплатной версии ngrok после каждого переподключения меняет url). Можно проект сходу разместить на сервер и этой проблемы не будет.

  • Реализуйте обработку events на бэкенде — и базовая настройка завершена.

Не забудьте передать user_id в checkout с фронтенда, чтобы было удобно идентифицировать пользователей на бекенде.

    if (user?.id) {
      checkoutOptions.customData = {
        user_id: String(user.id),
        plan_key: plan,
        plan_name: PLAN_NAMES[plan],
      } as CheckoutCustomData
    }

А также при реализации хендлеров для событий необходимо реализовать верификацию подписи с помощью notification secret key.

def verify_signature(raw_body: bytes, signature: str) -> bool:
    digest = hmac.new(WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256).digest()
    computed = base64.b64encode(digest).decode()
    return hmac.compare_digest(computed, signature)

Основные сущности

Также надо понимать основные сущности, которые создаёт Paddle, когда производит управление подписками:

  • Customers (клиент, которые покупает подписку. Ему присваивается customer_id);

  • Prices (Стоимость планов. У меня это 4 цены (Pro month, Pro year, Max month, Max year);

  • Products (Сами планы подписок Pro и Max соответственно);

  • Subscriptions (Подписка пользователя, свзана с ним по user_id);

  • Transactions (Все попытки оплаты пользователем и их статус).

Теперь перейдём к более интересным моментам, где у меня возникли вопросы. У меня совсем не было опыта интеграции платёжных систем, и вопросов у меня возникло довольно много. Разберём по порядку самые распространённые юзер-сценарии.


Сценарий 1. Первый апгрейд и proration

Прежде чем начать разбирать этот сценарий, нужно иметь понимание стратегий списания денежных средств при смене планов и что такое proration.

Proration — это перерасчёт денег при смене условий подписки не с границы периода (billing cycle), а в процессе. Смысл: дать кредит за неиспользованную часть старого плана и/или взять доплату за оставшуюся часть уже по новому плану.

Как это работает по сути:

  • Кредит: сколько «остаётся» от старого плана до конца оплаченного периода.

  • Дебет: сколько стоит новый план за тот же оставшийся кусок времени.

  • К оплате сейчас = Дебет − Кредит (если > 0). Если < 0, выходит «кредит/перенос» на будущее.

В Paddle это управляется параметром proration_billing_mode:

  • prorated_immediately — посчитать и списать разницу сейчас.

  • prorated_next_billing_period — посчитать сейчас, добавить в следующий счёт.

  • full_immediately — без прорации, сразу полная цена нового периода (старт цикла заново).

  • full_next_billing_period — без прорации, полная цена нового плана на следующем продлении. Но тогда юзер заплатит и за прошлый, и за текущий месяц.

  • do_not_bill — ничего отдельно не биллить за изменение; следующий счёт уже по новой конфигурации.

Итак, в нашем сценарии при первом переходе с Free на Pro month открывается checkout-страница, пользователь вводит данные карты и производит оплату. При этом Paddle обрабатывает платёж, создаёт сущности customer, transaction и subscription. На бэкенд
(в эндпоинт вебхука) поступают события (events), которые сообщают нашему бэкенду о создании/апдейте данных сущностей. С этого момента у пользователя появляется действующая подписка.

Потом пользователь хочет перейти с плана Pro month на Max month, потому что ваш сервис мега-крутой и пользователь упёрся в лимиты. И тут уже checkout открывать не требуется. Необходимо на бэкенде реализовать эндпоинт update_subscription, который делает запрос в Paddle и даёт команду на апдейт подписки и списание средств (Paddle после первого платежа сохраняет платёжное средство и потом с него списывает средства). В теле запроса шлём массив items, в котором передаётся новый price_id, который ссылается на продукт Max month. Документация по апдейту подписки.

При этом нам надо определить и указать proration_billing_mode. Лично я хочу, чтобы пользователю в таком кейсе пропорционально рассчитало списываемые средства. Вот так:

Был Pro $20/мес. Через 15 из 30 дней юзер апгрейдится на Max $30/мес.
Кредит за Pro ≈ $20 × (15/30) = $10.
Дебет за Max ≈ $30 × (15/30) = $15.
Итог сейчас: $15 − $10 = $5 к списанию сейчас.

Чтобы это реализовать, используем proration_billing_mode = prorated_immediately.

{
  "proration_billing_mode": "prorated_immediately",
  "items": [
    {
      "price_id": "pri_01gvne87kv8vbqa9jkfbmgtsed",
      "quantity": 1
    }
  ]
}

В таком случае при успешной оплате произойдёт апдейт подписки, и мы получим на вебхук эвенты subscription.updated и transaction.completed.

Лично я для себя решил, что внутри обработчиков событий я работаю только с теми сущностями, с которыми связано событие:
subscription.updated — работаю только с сущностью подписки;
transaction.completed — только с сущностью транзакции.

Окей, пользователь перешёл на Max, ему всё нравится, он доволен, но вдруг замечает, что ему было достаточно лимитов на Pro и решает вернуться обратно на этот план до окончания подписки Max.

Я считаю, что мы не можем просто так его перевести одномоментно и снова списать деньги, хотя это самый простой вариант. Я реализовал по-другому, более юзер-френдли, и сделал так, чтобы до конца периода пользователь был ещё на Max-подписке, а с нового периода уже перешёл на Pro. Тогда ему и доплачивать сейчас ничего не придётся, а только за следующий Pro-период.

Но тут есть проблема. Paddle не поддерживает запланированную смену плана (только отмену, но об этом позже): план там меняется сразу при апдейте (и, соответственно, приходит эвент subscription.updated). Но нам это в момент апдейт делать нельзя.

Как же это реализовать? Я добавил в сущность subscription новые поля типа scheduled_plan, scheduled_price_id и scheduled_date и изменил функционал обработки (вебхука) события subscription.updated в случае понижения плана + написал планировщик, который в установленную дату обновит план пользователя у нас в системе. Таким образом я реализовал запланированную смену плана в моём приложении.

У меня были вопросы, какой proration_billing_mode выбрать. На первых порах я допустил ошибку и поставил full_next_billing_period, думая, что пользователь заплатит полную стоимость нового плана в следующем billing cycle. Но оказалось, что ему было выставлено х2 стоимости Pro в следующем периоде, потому что он как бы саму смену плана посчитал как полную стоимость Pro + оплата Pro за следующий период. Такой вариант меня не устаивает, и нужно выбирать do_not_bill. Тогда смена плана произойдёт бесплатно, а пользователь заплатит одну стоимость Pro в следующем billing cycle.

Также необходимо добавить, что нельзя менять подписку, когда до биллинга < 30 минут — это необходимо учесть при апдейте подписки.


Сценарий 2. Отмена и возобновление

Разберём второй сценарий, когда пользователь находится на Pro и решает, что ему подписка не нужна (бывает и такое). В моём сценарии пользователя надо перевести обратно на free-план и отменить подписку.

Для отмены подписки есть 2 опции: либо отменить сразу, либо в конце текущего billing cycle (это регулируется параметром effective_from). Я реализовал второй вариант

"effective_from": "next_billing_period"

Для этого сделал эндпоинт cancel_subscription (документация). С параметром next_billing_period подписка сразу отменена не будет, а Paddle запланирует её к отмене по окончанию текущего billing cycle. Поэтому лучше сообщить это пользователю.

Также я добавил поле subscription.cancel_at_period_end + обработчик для события subscription.canceled, и когда текущий billing cycle истечёт, Paddle пришлёт это событие, и у нас на бэкенде произойдёт отмена подписки (состояние синхронизируется с данными в системе Paddle).

Далее по сценарию пользователь всё-таки решает оставить подписку — для этого я реализовал эндпоинт resume_subscription и просто отменяю отмену, устанавливая

"scheduled_change": null

Сценарий 3. Переход на годовой Max

Пользователь полностью доволен сервисом и решает приобрести максимальный план подписки на год. Тут у нас впервые идёт смена billing cycle при апдейте подписки. По сути обработка не сильно меняется: мы также устанавливаем proration_billing_mode = prorated_immediately, потому что нам надо, чтобы пользователь заплатил разницу при повышении стоимости плана, и также передаём в payload items с price_id для Max (year).

Но тут есть один важный нюанс. При смене billing cycle мы можем использовать только proration_billing_mode:

  • prorated_immediately,

  • full_immediately,

  • do_not_bill

Сценарий 4. Отложенная смена подписки и отмена

В данном сценарии в целом все действия уже были описаны в предыдущих сценариях, но тут стоит обратить внимание на сочетание запланированной отмены подписки и потом попытку её совсем отменить. Тут просто в приоритет ставим отмену и по итогу обрабатываем event subscription.canceled. Нужно только в планировщике поставить ограничение, чтобы он не апдейтил записи (переводил на запланированный план), которые имеют дату отмены. А также на фронте выводить только один информационный блок — либо с «Ваша подписка будет отменена такого-то числа», либо «Вы перейдёте на plan pro такого-то числа».


Сценарии 5–6. Одновременная смена плана и billing cycle

Это довольно редкие кейсы, но их также надо уметь обрабатывать. Особенностью этих сценариев является одновременная смена плана и billing cycle.

  • Кейс 5 — понижение плана и увеличение billing cycle. В моём случае стоимость Max month дешевле, чем Pro year, поэтому просто берём proration_billing_mode = do_not_bill и передаём items с новым price_id, и подписка обновится с начала нового billing cycle с помощью уже реализованной системы.

  • Кейс 6 — повышение плана и billing cycle, так что мы просто, как и в кейсе 1, берём разницу между стоимостью планов с помощью proration_billing_mode = prorated_immediately.


Сценарии 7–8. Неудачная оплата

И перейдём к заключительному этапу — это обработка неудачной оплаты.

  • Кейс 7 — всё просто: подписка тут в принципе не создаётся из-за неудачной оплаты. На бэкенде фиксируем только transaction (придёт event о transaction.payment_failed).

  • Кейс 8 — чуть сложнее. Пользователь уже имеет оплаченную подписку, и в случае, если оплата за следующий период не прошла, в системе Paddle подписка перейдёт в статус past_due, и на бэкенд придёт событие subscription.past_due. Обрабатываем это событие, ставим grace period для подписки. В течение grace period Paddle ещё до 7 раз попытается списать средства у пользователя, и если попытки также будут неудачными, подписка будет отменена в течение 30 дней.

Однако нужно также держать в голове, что может, допустим, с третьей попытки пройти оплата, и тогда придёт событие subscription.activated. Для такого кейса нам надо специально добавить обработку, чтобы обнулить поля grace_period_ends_at, payment_failed_at, payment_retry_count (эти поля я специально добавил к сущности подписки для обработки неудачных оплат).

Customer portal

Таким образом мы прошли все наиболее часто встречающиеся пользовательские сценарии, но есть ещё момент, который стоит подсветить.

Допустим, пользователь захочет обновить средство оплаты. Для этого Paddle предоставляет свой механизм, так называемый customer portal. Я на фронте добавил кнопку Update payment method и просто перенаправляю пользователя на страницу Paddle customer portal. Ссылку можно получить путём POST-запроса на адрес /customers/{customer_id}/portal-sessions либо из сущности подписки в management_urls. В customer portal можно не только обновить метод платежа, но также посмотреть текущую подписку и все прошлые платежи.

Что дальше

Сейчас я закончил интеграцию платежной системы, и занимаюсь непосредственно разработкой продукта. Если кому-то интересно, вот мой телеграм канал, где я рассзываю о процессе создания проекта в ограниченное время и паралельно формирую систему (фреймворк) серийного запуска полезных продуктов. Планирую в будущем написать также отдельную статью о том, что надо чтобы пройти KYC и иметь возможность уже полноценно получать платежи.

Комментарии (4)


  1. ihouser
    10.10.2025 13:01

    Я платежных системам не делал, поэтому есть, возможно наивный, вопрос: зачем так заморачиваться? Просто есть счет, на который поступает деньги и счетчик, который минусует по капле в соответствии с коэффициентом плана. А пользователь пускай меняет план (коэффициент) пока не надоест.
    В чем я ошибаюсь?


    1. SsdPie Автор
      10.10.2025 13:01

      Ну это просто совсем другой подход. Не система подписок, а баланс, который пользователь тратит. Не каждый бизнес может позволить такое сделать, но в целом путь намного проще и имеет место быть. Я решил сразу сделать полноценную систему подписок.


      1. ihouser
        10.10.2025 13:01

        Вот этого я не понимаю. Баланс все равно должен присутствовать в системе. А подписка - просто циферка в аккаунте. Пользователь ведь будет видеть что подписался на одну из.

        Чем таким "полноценная" отличается от простой, что стоит таких усилий?


        1. SsdPie Автор
          10.10.2025 13:01

          ну допустим если нужно разграничить доступ на разные фичи. К примеру как gpt-5 pro доступна только на определенных уровнях подписки. Есть еще бизнес преимущества, легче предсказывать ARR. И есть в целом продукты, которые проще сделать с подписками, к примеру Яндекс Плюс, Netflix, Spotify, потому что там доступ на период, а не расход ресурса.