03 мая 2025 г.·7 мин

Оплата по использованию через Stripe: практическая модель данных

Оплата по использованию через Stripe требует чистого хранения событий и сверки. Узнайте простую схему, поток вебхуков, бэктфиллы и способы избежать двойного учёта.

Оплата по использованию через Stripe: практическая модель данных

Что вы на самом деле строите (и почему это ломается)

Оплата по использованию звучит просто: измерь, сколько клиент использовал, умножь на цену и выставь счёт в конце периода. На практике вы строите небольшую систему учёта. Она должна оставаться корректной даже когда данные приходят с опозданием, приходят дважды или вовсе не приходят.

Большинство ошибок происходит не на этапе оформления заказа или в панели управления. Они происходят в модели данных учёта. Если вы не можете уверенно ответить на вопрос «Какие события использовались для этого счёта и почему?», то рано или поздно вы будете переплачивать, недоплачивать или терять доверие.

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

Stripe отлично справляется с ценами, счетами, налогами и сбором денег. Но Stripe не знает сырых событий вашего продукта, если вы их не отправите. Это заставляет принять решение, где хранится источник правды: в Stripe как в книге учёта или в вашей базе данных, которую отражает Stripe?

Для большинства команд безопасное разделение такое:

  • Ваша база данных — источник правды для сырых событий использования и их жизненного цикла.
  • Stripe — источник правды для того, что реально выставлено в счётах и оплачено.

Пример: вы отслеживаете «API вызовы». Каждый вызов порождает событие использования со стабильным уникальным ключом. В момент выставления счёта вы суммируете только подходящие события, которые ещё не были учтены, затем создаёте или обновляете позицию в счёте Stripe. Если при приёме повторно пришло то же событие или webhook доставлен дважды, правила идемпотентности делают дубликат безопасным.

Решения, которые нужно принять до проектирования таблиц

Прежде чем создавать таблицы, зафиксируйте определения, которые потом будут решать, останется ли биллинг объяснимым. Большинство «таинственных багов со счётами» вызваны не плохим SQL, а нечёткими правилами.

Начните с единицы измерения, за которую вы берёте плату. Выберите то, что легко измерить и сложно оспорить. «API вызов» осложняется повторами, пакетными запросами и ошибками. «Минуты» усложняются перекрытиями. «ГБ» требует ясной базы (GB vs GiB) и метода измерения (среднее или пиковое).

Далее определите границы окна. Система должна точно знать, к какому окну относится событие: час, день, расчётный период или действие пользователя? Если клиент меняет план в середине месяца, вы делите окно или применяете одну цену к целому месяцу? Эти выборы определяют, как вы группируете события и как объясняете итоги.

Также решите, какая система владеет какими фактами. Распространённый паттерн со Stripe: ваше приложение владеет сырыми событиями и вычисленными итогами, а Stripe владеет счетами и статусом оплаты. Такой подход лучше работает, если вы не правите историю «молча». Исправления записываются как новые записи, а оригинал сохраняется.

Короткий набор неоспоримых требований поможет держать схему честной:

  • Прослеживаемость: каждая выставленная единица может быть связана с сохранёнными событиями.
  • Аудируемость: вы можете ответить «почему это было взимано?» через месяцы.
  • Обратимость: ошибки исправляются явными корректировками.
  • Идемпотентность: один и тот же ввод не может считаться дважды.
  • Чёткая ответственность: каждый факт принадлежит одной системе (использование, ценообразование, выставление счётов).

Пример: если вы берёте плату за «отправленные сообщения», решите, считаются ли повторы, засчитываются ли неудачные доставки и какой временной штамп главный (время клиента или сервера). Запишите это и затем закодируйте в полях событий и валидации, а не оставляйте в памяти людей.

Простая модель данных для событий использования

Оплата по использованию проще, когда вы относитесь к использованию как к бухгалтерии: сырые факты — append-only, итоги — производные. Этот шаг предотвращает большинство споров, потому что вы всегда можете объяснить, откуда взялась цифра.

Практическое начало использует пять основных таблиц (имена могут варьироваться):

  • customer: внутренний id клиента, Stripe customer id, статус, базовая метаинформация.
  • subscription: внутренний id подписки, Stripe subscription id, ожидаемый план/цены, временные метки начала/окончания.
  • meter: что вы измеряете (API вызовы, места, GB-часы). Включите стабильный ключ метра, единицу и метод агрегации (sum, max, unique).
  • usage_event: одна строка на действие. Храните customer_id, subscription_id (если известно), meter_id, quantity, occurred_at (когда произошло), received_at (когда вы это приняли), source (app, batch import, partner) и стабильный внешний ключ для дедупа.
  • usage_aggregate: вычисленные итоги, обычно по customer + meter + временной корзине (день или час) и расчётному периоду. Храните суммарное количество плюс версию или last_event_received_at для поддержки перерасчёта.

Держите usage_event неизменяемым. Если позже обнаружите ошибку, запишите компенсирующее событие (например, -3 места при отмене), вместо редактирования истории.

Храните сырые события для аудита и споров. Если вы не можете хранить их навсегда, держите их хотя бы в пределах вашего окна возврата по биллингу плюс периода возврата/оспаривания.

Держите вычисляемые итоги отдельно. Агрегаты нужны для быстрого выставления счётов и дашбордов, но они расходуемые. Вы должны уметь пересобрать usage_aggregate из usage_event в любой момент, включая после бэктфилла.

Идемпотентность и состояния жизненного цикла события

Данные использования шумные. Клиенты повторяют запросы, очереди доставляют дубликаты, а вебхуки Stripe могут приходить не по порядку. Если база не может доказать «это событие уже учтено», вы в конце концов выставите счёт дважды.

Дайте каждому событию стабильный детерминированный event_id и обеспечьте уникальность по нему. Не полагайтесь только на автоинкрементный id как единственный идентификатор. Хороший event_id выводится из бизнес-действия, например customer_id + meter + source_record_id (или customer_id + meter + timestamp_bucket + sequence). Если то же действие отправлено снова, оно даёт тот же event_id, и вставка становится безопасным noop.

Идемпотентность должна охватывать все пути приёма, а не только публичный API. SDK-вызовы, пакетные импорты, воркеры и обработчики вебхуков — всё это может быть ретраено. Используйте одно правило: если ввод может быть повторён, ему нужен ключ идемпотентности, сохранённый в базе и проверяемый перед изменением итогов.

Простая модель состояний жизненного цикла облегчает ретраи и поддержку. Держите её явной и сохраняйте причину при ошибке:

  • received: сохранено, ещё не проверено
  • validated: прошло схему, проверку клиента, метра и правил временного окна
  • posted: учтено в итогах биллинга
  • rejected: окончательно игнорируется (с кодом причины)

Пример: воркер упал после валидации, но до постановки в итог. При повторе он находит тот же event_id в состоянии validated и продолжает до posted без создания второго события.

Для вебхуков Stripe используйте тот же паттерн: храните event.id от Stripe и помечайте его обработанным только один раз, чтобы дубликаты доставки были безопасны.

Пошагово: приём событий учёта от начала до конца

Ship the metering backend
Stand up a production backend for metering, reconciliation, and audit trails without hand-coding.
Create Backend

Относитесь к каждому событию учёта как к деньгам: проверьте, запишите оригинал, затем вычислите итоги из источника правды. Это делает биллинг предсказуемым при ретрайях или поздних данных.

Надёжный поток приёма

Проверяйте каждое входящее событие до того, как трогать итоговые таблицы. Минимальные требования: стабильный идентификатор клиента, имя метра, числовое количество, временная метка и уникальный ключ события для идемпотентности.

Сначала запишите сырое событие, даже если вы планируете агрегировать позже. Эта сырая запись — то, что вы будете перепроцессировать, аудировать и использовать для исправлений без домыслов.

Надёжный поток выглядит так:

  • Примите событие, проверьте обязательные поля, нормализуйте единицы (например, секунды vs минуты).
  • Вставьте строку с сырым событием, используя ключ события как уникальное ограничение.
  • Агрегируйте в корзину (дневную или расчётную) применив количество события.
  • Если вы отправляете использование в Stripe, зафиксируйте, что отправляли (meter, quantity, period и идентификаторы ответа Stripe).
  • Логируйте аномалии (отклонённые события, конверсии единиц, поздние поступления) для аудита.

Держите агрегацию повторабельной. Частый подход: вставить сырое событие в одной транзакции, затем поставить задачу в очередь для обновления корзин. Если задача выполнится дважды, она должна обнаружить, что сырое событие уже применено.

Когда клиент спросит, почему ему выставлен счёт за 12 430 API вызовов, вы должны показать точный набор сырых событий, включённых в это окно биллинга.

Сверка вебхуков Stripe с вашей базой данных

Harden webhook processing
Store Stripe webhook events once and process them safely even when they arrive twice.
Set Up Webhooks

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

Большинство команд фокусируется на небольшом наборе типов вебхуков, которые влияют на результат биллинга:

  • invoice.created, invoice.finalized, invoice.paid, invoice.payment_failed
  • customer.subscription.created, customer.subscription.updated, customer.subscription.deleted
  • checkout.session.completed (если вы начинаете подписки через Checkout)

Сохраняйте каждый полученный вебхук. Храните сырую полезную нагрузку, а также то, что вы наблюдали при получении: Stripe event.id, event.created, результат проверки подписи и время получения сервером. Эта история важна при отладке несоответствий или ответе на «почему меня так сняли?»

Надёжный идемпотентный шаблон сверки выглядит так:

  1. Вставьте вебхук в таблицу stripe_webhook_events с уникальным ограничением по event_id.
  2. Если вставка терпит неудачу — это повтор. Остановитесь.
  3. Проверьте подпись и зафиксируйте pass/fail.
  4. Обработайте событие, найдя внутренние записи по Stripe ID (customer, subscription, invoice).
  5. Применяйте изменения состояния только если они переводят запись вперёд.

Доставка не по порядку — нормальна. Используйте правило «максимальное состояние выигрывает» плюс временные метки: никогда не переводите запись назад.

Пример: вы получили invoice.paid для счёта in_123, но внутренней строки для этого счёта ещё нет. Создайте строку с пометкой «встретили из Stripe», затем прикрепите её к нужному аккаунту позже по Stripe customer ID. Это сохраняет вашу книгу учёта согласованной без двойной обработки.

От итогов использования до позиций в счёте

Преобразование сырых событий в позиции счёта в основном о синхронизации времени и границах. Решите, нужны ли вам итоги в реальном времени (дашборды, оповещения о расходах) или только в момент биллинга (счета). Многие команды делают и то, и другое: записывают события постоянно, а итоговые значения для счёта вычисляют по расписанию.

Согласуйте окно использования с расчётным периодом Stripe. Не угадывайте календари. Используйте период начала и конца биллинга у subscription item, затем суммируйте только события с временными метками внутри этого окна. Храните временные метки в UTC и делайте окно биллинга в UTC.

Храните историю неизменной. Если позже найдёте ошибку, не редактируйте старые события и не переписывайте прошлые итоги. Создайте запись корректировки, которая указывает на оригинальное окно и добавляет или вычитает количество. Это проще для аудита и объяснений.

Плановые изменения и прорейшены — место, где прослеживаемость часто теряется. Если клиент меняет план в середине цикла, разбейте использование на подокна, соответствующие активному диапазону каждой цены. Ваш счёт может содержать две позиции использования (или одну позицию и корректировку), каждая привязанная к цене и временному диапазону.

Практический поток:

  • Возьмите окно счёта из period start и end от Stripe.
  • Аггрегируйте подходящие события использования в итог для этого окна и цены.
  • Сгенерируйте позиции счёта из итогов использования и корректировок.
  • Сохраните идентификатор прогона расчёта, чтобы можно было позже воспроизвести числа.

Бэктфиллы и поздние данные без потери доверия

Standardize your schema
Reuse a consistent pattern for events, aggregates, and adjustments across every new meter.
Use Templates

Поздние данные — нормальны. Устройства уходят офлайн, пакетные задания задерживаются, партнёры повторно шлют файлы, логи воспроизводятся после сбоя. Главное — относиться к бэктфиллам как к работе по исправлению, а не способу «подогнать числа».

Явно указывайте, откуда могут приходить бэктфиллы (логи приложения, экспорт из хранилища, системы партнёров). Записывайте источник у каждого события, чтобы объяснить, почему оно пришло поздно.

При бэктфилле храните два временных поля: когда событие произошло (время для биллинга) и когда вы его приняли. Пометьте событие как backfilled, но не перезаписывайте историю.

Предпочитайте пересборку итогов из сырых событий вместо применения дельт к сегодняшним агрегатам. Реплеи позволяют восстановиться после багов без домыслов. Если ваш пайплайн идемпотентен, вы можете прогнать день, неделю или весь расчётный период и получить те же итоги.

После того как счёт создан, корректировки должны следовать понятной политике:

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

Пример: партнёр присылает недостающие события за 28–29 января 3 февраля. Вы вставляете их с occurred_at в январе, ingested_at в феврале, source = "partner". Январский счёт уже оплачен, поэтому вы создаёте небольшой дополнительный счёт за недостающие единицы с причиной, сохранённой рядом с записью сверки.

Распространённые ошибки, которые приводят к двойному учёту

Двойной учёт случается, когда система воспринимает «сообщение пришло» как «действие произошло». С повторами, поздними вебхуками и бэктфиллами нужно отделять действие клиента от вашей обработки.

Типичные виновники:

  • Повторы трактуются как новое использование. Если у каждого события нет стабильного action id (request_id, message_id) и база не обеспечивает уникальность, вы посчитаете дважды.
  • Время события смешивают со временем обработки. Отчёт по времени приёма вместо времени события приводит к тому, что поздние события попадают в неправильный период и затем учитываются снова при реплее.
  • Сырые события удаляются или перезаписываются. Если у вас только скользящий итог, вы не сможете доказать, что произошло, и повторная обработка может раздуть суммы.
  • Предположение о порядке вебхуков. Вебхуки могут дублироваться, приходить не по порядку или отражать частичные состояния. Сверяйте по Stripe object IDs и держите защиту «уже обработано».
  • Отмена, возвраты и кредиты не моделируются явно. Если вы только добавляете использование и никогда не записываете отрицательные корректировки, то при попытке «исправить» вы будете снова считать данные.

Пример: вы записали «10 API вызовов» и позднее оформили кредит на 2 вызова из-за сбоя. Если вы при бэктфилле снова отправите весь день и одновременно примените этот кредит, клиент увидит 18 вызовов (10 + 10 - 2) вместо ожидаемых 8.

Быстрая проверка перед запуском

Build a usage ledger
Model raw usage events, aggregates, and adjustments with the Data Designer in minutes.
Try AppMaster

Прежде чем включать биллинг по использованию для реальных клиентов, прогоните последний чеклист по базовым вещам, которые предотвращают дорогие баги. Большинство проблем — не «ошибки Stripe», а проблемы с данными: дубликаты, пропущенные дни и молчаливые повторы.

Короткий и обязательный чеклист:

  • Обеспечьте уникальность событий использования (например, уникальный constraint на event_id) и придерживайтесь одной стратегии id.
  • Храните каждый вебхук, проверяйте подпись и обрабатывайте идемпотентно.
  • Относитесь к сырым событиям как к неизменяемым. Исправляйте с помощью корректировок (положительных или отрицательных), а не правкой исторических строк.
  • Запускайте ежедневную сверку, сравнивая внутренние итоги (по клиенту, метру, дню) со статусом биллинга в Stripe.
  • Добавьте оповещения на пропуски и аномалии: отсутствующие дни, отрицательные итоги, резкие всплески или большая разница между «событий принято» и «событий выставлено в счёт».

Простой тест: выберите одного клиента, перезапустите приём за последние 7 дней и убедитесь, что итоги не изменятся. Если меняются — у вас ещё есть проблемы с идемпотентностью или состояниями жизненного цикла.

Пример сценария: реальный месяц использования и счетов

Go live with confidence
Deploy your billing service to AppMaster Cloud or your own cloud when you are ready.
Deploy Now

Небольшая служба поддержки использует портал клиента, который берёт $0.10 за обработанный разговор. Они продают это как оплату по использованию через Stripe, но доверие строится на том, что происходит, когда данные становятся неряшливыми.

1 марта клиент начинает новый расчётный период. Каждый раз, когда агент закрывает разговор, ваше приложение посылает событие использования:

  • event_id: стабильный UUID от вашего приложения
  • customer_id и subscription_item_id
  • quantity: 1 разговор
  • occurred_at: время закрытия
  • ingested_at: когда вы это впервые увидели

3 марта фоновой воркер из-за таймаута повторил отправку того же разговора. Поскольку event_id уникален, вторая вставка становится noop и итоги не меняются.

В середине месяца Stripe посылает вебхуки о превью счёта и затем о финализированном счёте. Обработчик вебхуков сохраняет stripe_event_id, type и received_at, и помечает его обработанным только после коммита транзакции в базе. Если вебхук доставлен дважды, вторая доставка игнорируется, потому что stripe_event_id уже существует.

18 марта вы импортируете позднюю партию из офлайн-клиента: 35 разговоров за 17 марта. Эти события имеют старые occurred_at, но они валидны. Система вставляет их, пересчитывает дневные итоги за 17 марта, и дополнительное использование попадает в следующий счёт, потому что период биллинга ещё открыт.

22 марта вы обнаруживаете, что один разговор был записан дважды из-за бага, который сгенерировал два разных event_id. Вместо удаления истории вы пишете корректировку с quantity = -1 и причиной «duplicate detected». Это сохраняет аудит-трейл и делает изменение счёта объяснимым.

Следующие шаги: реализуйте, мониторьте и улучшайте

Начните с малого: один метр, один план, один сегмент клиентов, который вы хорошо понимаете. Цель — простая согласованность: ваши числа совпадают со Stripe месяц за месяцем, без сюрпризов.

Постройте мало, затем укрепляйте

Практический первичный запуск:

  • Определите одну форму события (что считается, в какой единице, по какому времени).
  • Храните каждое событие с уникальным ключом идемпотентности и явным статусом.
  • Аггрегируйте в ежедневные (или почасовые) итоги, чтобы можно было объяснить счёт.
  • Проводите сверку со Stripe по расписанию, а не только в реальном времени.
  • После выставления счёта считайте период закрытым и направляйте поздние события через путь корректировок.

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

Мониторинг, который спасает вас позже

Добавьте простые экраны аудита сразу. Они окупаются с первого же раза, когда кто-то спросит: «Почему мой счёт в этом месяце больше?» Полезные представления: поиск событий по клиенту и периоду, дневные итоги по периоду, статус обработки вебхуков и обзор бэктфиллов и корректировок с информацией кто/когда/почему.

Если вы реализуете это в AppMaster (appmaster.io), модель ложится естественно: определите сырые события, агрегаты и корректировки в Data Designer, затем используйте Business Processes для идемпотентного приёма, плановой агрегации и сверки вебхуков. Вы всё ещё получаете реальную книгу учёта и аудит-трейл без ручной прописи всей инфраструктуры.

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

Вопросы и ответы

Why does usage-based billing with Stripe feel harder than it sounds?

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

Should Stripe or my database be the source of truth for usage?

Безопасный подход: ваша база данных — источник правды для сырых событий использования и их статусов, а Stripe — источник правды для выставленных счетов и результатов оплат. Такое разделение сохраняет прослеживаемость, а Stripe берёт на себя цены, налоги и сборы.

What makes a good idempotency key (event_id) for a usage event?

Сделайте его стабильным и детерминированным, чтобы повторы давали тот же идентификатор. Часто он строится из реального бизнес-действия, например customer_id + meter_key + source_record_id, тогда повторная отправка становится безопасным no-op.

How do I fix incorrect usage without rewriting history?

Не редактируйте и не удаляйте принятые события использования. Запишите компенсирующее событие (например, с отрицательным количеством) и сохраните оригинал — так вы сохраните историю и сможете объяснить изменения позже.

Do I really need both raw events and aggregated totals?

Храните сырые события неизменяемыми и держите агрегаты отдельно как вычисляемые данные, которые можно пересобрать. Агрегаты нужны для скорости и отчётности; сырые события — для аудита, споров и восстановления после ошибок.

How should I handle late-arriving usage data or backfills?

Храните как минимум два временных показателя: когда событие произошло (occurred_at) и когда вы его приняли (ingested_at), а также источник. Если счёт ещё не финализирован — пересчитайте перед финализацией; если финализирован — оформляйте корректировку явно (дополнительный счёт или кредит), а не перемещайте использование в другой период.

What’s the safest way to process Stripe webhooks without duplicates?

Сохраняйте каждую полезную нагрузку вебхука и применяйте идемпотентную обработку, используя event.id от Stripe как уникальный ключ. Вебхуки часто дублируются или приходят не по порядку, поэтому обработчик должен применять изменения только если они переводят запись вперёд по состоянию.

How do I handle upgrades, downgrades, and proration with usage meters?

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

How can I tell if my metering pipeline is safe before going live?

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

Can I build this data model and workflow in AppMaster without custom code?

Спроектируйте таблицы для сырых событий, агрегатов, корректировок и приёмника вебхуков в Data Designer, затем реализуйте приём и согласование в Business Processes с ограничениями уникальности для идемпотентности. Так можно получить аудиторский журнал и запланированную сверку без ручного написания всей логики.

Легко начать
Создай что-то невероятное

Экспериментируйте с AppMaster с бесплатной подпиской.
Как только вы будете готовы, вы сможете выбрать подходящий платный план.

Попробовать AppMaster