1
0 Comments

Implementing stripe revenue attribution in a web analytics tool. The dirty implementation parts...

Quick technical writeup. Building Zenovay, a web analytics tool. One of our main features is revenue attribution: which marketing channel actually produced this stripe payment.

Sounds simple. The clean version is:

  1. Visitor lands on site with utm params
  2. We assign them a first party session id (no cookies)
  3. They eventually pay through stripe
  4. Stripe webhook fires, we look up the session id, attribute the payment

The actual implementation has at least 6 problems the clean version hides.

Problem 1: visitor identity persistence without cookies.
We use a first party session id stored in localStorage. Stable across the same browser, lost across devices. The mental model is 'session = browser instance', not 'user'. This means cross device journeys are partially attributed at best. We accepted this tradeoff for the privacy story.

Problem 2: the stripe customer object often has no link back to the website session
Stripe knows the customer email and the payment metadata. It does not know the session id unless we put it there. So we have to inject the session id into stripe metadata at checkout creation time. This requires the customer to have integrated our js sdk with their stripe checkout flow. The integration is one line, but it's a load bearing line.

Problem 3: webhook race conditions.
A payment fires 'payment_intent.succeeded' before 'checkout.session.completed' in some cases. If you naively attribute on the first event, you miss the metadata that arrived on the second. We queue and dedupe by payment_intent_id with a 60 second debounce.

Problem 4: refunds and disputes.
If a payment is refunded, do you reverse the attribution? We chose yes. Means we send a 'negative' event downstream that subtracts from the channel's attribution. This is obvious in retrospect but the implementation needs idempotency keys per refund event.

Problem 5: subscriptions.
Recurring revenue attribution is the worst part. Do you attribute the entire mrr to the original session forever? Just the first payment? Some decaying function? We picked 'first payment fully attributed, recurring goes to a separate ltv bucket'. Argueable. Our customers asked for both views.

Problem 6: timezone handling for daily revenue reports.
Stripe gives utc. Customers want their local timezone. We push timezone normalisation to the edge of the data path, not the query path, because doing it at query time killed dashboard performance on month long ranges.

Stack wise the whole pipeline runs on cloudflare workers with d1 + r2 for the long term store. The webhook handler is a separate worker that validates the stripe signature, dedupes, writes to a queue, and lets the analytics worker consume.

What i wish i had known before building this:

  • write the negative test cases first (refunds, disputes, partial refunds). they are the hardest to get right and the most embarrassing to get wrong
  • document the customer integration on day one. the one line they add to their stripe flow must be impossible to get wrong
  • decide your subscription attribution model before you build anything. retrofitting it is painful

Tool is at zenovay.com. Happy to dig into specifics if anyone is building similar attribution.

on May 30, 2026
Trending on Indie Hackers
How to rank #1 on ChatGPT? User Avatar 111 comments I Tested Agenmatic for Finding Customers in Communities — Here’s What I Learned User Avatar 63 comments I built a startup-idea scanner. It just told me none of my 3,400 ideas are easy wins. User Avatar 51 comments A chat assistant that runs your server so you don't have to live in the terminal User Avatar 43 comments Building a Shopify bundles app for stores with real fulfillment: here's the wedge User Avatar 42 comments “I’ll just post on Upwork” is not a client strategy. Here’s what I built instead. User Avatar 31 comments