Hur Abbas

Revenue data

A revenue ledger between Stripe and the CRM

Billing data reached the CRM through three separate workflows, and Finance still tracked revenue contraction by hand every week. I redesigned it as one model: live subscriptions, a permanent event ledger, and daily netted MRR movements.

Role
Architect and builder
Company
RepairDesk
Status
Contraction live; full sync in progress
Built with
Stripe API, webhooks, n8n, Zoho CRM

The problem

Over time, separate workflows had grown up for subscriptions, invoices and contraction. Each solved one need. None agreed on what a subscription change meant, and every new question from Finance meant another workflow.

The most painful gap was contraction: customers paying less but not leaving. Finance pulled it from Stripe every week and filed it by hand.

Constraints

  • A customer can have several subscriptions, each with several items.
  • Stripe sends events out of order, sometimes twice, and retries for up to three days.
  • Discount-only changes arrive without the previous values, so there is nothing to compare against.
  • An existing CRM module and its reports had to keep working.

Definitions first

Before any code, I agreed the terms with the people who report on them. Contraction means an account pays less but still has an active subscription. Churn means every subscription on the account is gone. The first version replayed events in order to decide; the final rule is simpler and sturdier: look at the account's total MRR at the daily cutoff. Zero is churn. Anything else is contraction.

What I built

Stripe eventsWebhooksEvent ledgerStored raw, foreverDaily nettingPer subscriptionMRR movementsNetted, categorizedReviewSubscriptionsCurrent stateUnmatchedWorklist + alertAccount total at cutoff:0 means churn, otherwise contraction
Events are captured instantly but netted once a day, which absorbs duplicates and out-of-order delivery.

The design has three layers, each with one job:

SubscriptionsWhat each customer pays for right now. Overwritten as things change.Subscription eventsEvery billing change, kept forever. A processed flag turns the table into a work queue.MRR movementsOne netted record per subscription per day, only when MRR actually moved. What Finance reports on.
  • Capture. Every Stripe event is stored as it arrives, with a unique key so duplicates are ignored. Instead of trusting the payload, the workflow re-fetches the event from Stripe.
  • Net. Once a day, each subscription's events are reduced to a single movement using its first and last state, so swaps, duplicates and retries cancel out.
  • Review. Movements carry a needs-attention flag and an exclude option, so Finance reviews exceptions instead of compiling the list.
  • Unmatched customers. Anything that can't be linked to an account is kept, flagged and sent to a saved worklist with an alert. Nothing disappears.

What broke, and how I fixed it

  • Plan swaps doubled starting MRRWhen one item replaced another, both were counted as the starting point. Moving to first-and-last netting removed the double count.
  • Starting MRR silently became zeroOne missing line meant the previous state was never saved, so every movement looked new. It surfaced only when totals failed to reconcile.
  • One duplicate row, one missing rowOn multi-item changes, the sheet matched rows on too few columns. Adding the item and import date to the match key fixed both at once.
  • Coupons changed MRR but produced nothingStripe omits the old values on discount-only changes. A parallel branch now handles discount events directly.
  • Dates rejected by the CRMOne endpoint wanted an explicit offset where another wanted none. Small, but it stopped the whole sync until found.

Outcome

~20 / weekcontraction records created automatically in 2026
~2,900subscriptions mirrored into the CRM
300+monthly billing summaries posted to accounts

Finance moved from compiling contraction to reviewing it. The new model gives every future billing question one place to look, instead of another workflow.

What I'd do next

Switch on real-time capture and daily netting for all events, retire the spreadsheet stage, then add failed-payment history per subscription so Success sees billing risk next to everything else.