Stripe revenue attribution: 3 ways to tie payments to traffic
How to tie every Stripe charge, renewal and refund to the channel, campaign and landing page that brought the customer, with identify, metadata and UTM tags.
Stripe revenue attribution means knowing, for each Stripe charge, which traffic source, campaign and landing page brought the person who paid. Not just the first payment. The renewals that follow, the refund three weeks later and the dispute in month four should all sit on the same channel, so the channel's total is what you kept.
Stripe alone cannot tell you this. Stripe knows the customer and the money: the customer id, the email, the amount, the fee, the subscription and every refund. It does not know the visit. It never saw the ad click, the newsletter link or the search result that started the journey. That happened on your site, before checkout. Stripe revenue attribution by traffic source is a join between the two, and this guide covers how ProductLense makes it. For the wider picture across providers, see the pillar on revenue attribution for SaaS. If you take payments in India through Razorpay, see Razorpay conversion tracking.
The ways to tie a Stripe payment to a visit
ProductLense resolves every payment through one chain:
payment → customer → identity → visitor → first touchEach Stripe payment carries its Stripe customer. The open question is the middle link: how does a Stripe customer become a visitor your site recorded? ProductLense has three documented answers, tried in this order until one finds a visitor with a first touch.
1. Your user id on the Stripe customer
This is the strongest link. After sign-up, your site calls identify with the user's id from your own database. When you create the Stripe customer, put the same id in its metadata under pl_user_id.
window.productlense('identify', 'user_123', { email: '[email protected]' });const customer = await stripe.customers.create({
email: user.email,
metadata: { pl_user_id: user.id },
});ProductLense reads the id from the customer metadata in this order: pl_user_id, user_id, userId, uid. It then finds every visitor that identified with that id and takes the earliest first touch among them, so a laptop visit and a later phone visit count as one person. The id also survives an email change.
2. The customer email
When the Stripe customer carries no user id, ProductLense falls back to the customer's email. It matches the email to the email trait you sent with identify, or to a user id that is itself that email. The match is case-insensitive.
This catches customers whose billing record was created where you could not set metadata, as long as they pay with the email they signed up with. Send email in the identify traits even when you set the id: the id is the primary link, the email is the safety net. A ProductLense visitor id under pl_visitor_id or visitor_id on the customer is also accepted.
3. The Payment API for anything Stripe does not carry
Some revenue never passes through Stripe, such as an invoice paid by bank transfer. The Payment API takes those payments one at a time with POST https://api.productlense.app/api/v1/payments, and each goes through the same chain as a Stripe charge. You send customer.user_id or customer.email, and the payment is attributed like a connector payment. Refunds go in as their own payment with kind: "refund" and the same customer.id, so they land on the same channel. The Payment API needs an API key with the payments scope, part of the Scale plan.
Whatever the method, the answer is stored on the payment row when it is written, so reports never re-run attribution.
Track Stripe payments with UTM parameters
Founders often ask how to pass UTM parameters into Stripe. You do not have to. The UTM tags live on the link in the ad, the newsletter or the social post. When the visitor lands on your site, the ProductLense script reads the tags from the landing URL and stores them on that visitor's first touch: channel, utm_source, utm_medium, utm_campaign, referrer domain and landing path. That first touch is kept for the life of the site, after raw events expire.
Stripe never needs to see the tags. The payment finds its way back to the first touch through the customer. A tagged link looks like this:
https://example.com/pricing?utm_source=newsletter&utm_medium=email&utm_campaign=october-launchA few rules keep the join clean:
- Source and medium are lower-cased for you. Campaign keeps its case, so
Launchandlaunchare two campaigns. Pick lower case with hyphens. utm_mediumdecides the channel:cpc,ppc,paid,paidsocialanddisplayarepaid, andemailisemail. Agclid,fbclidormsclkidalso marks the visit aspaid.- Do not tag links between your own pages. The tag would be recorded on those pageviews and muddy the source report.
When a buyer returns from checkout.stripe.com to your success page, ProductLense treats that referrer as no referrer, so the return from checkout never counts as a referral visit.
Connect Stripe to ProductLense
The connection is read-only. ProductLense never writes to your Stripe account and never moves money. The steps follow the Stripe doc, and the Stripe integration page has the same list.
- In Stripe, open Developers → API keys → Create restricted key. Give it Read access to Customers, Subscriptions, Charges, Refunds, Balance transactions, Invoices, PaymentIntents and Checkout Sessions. ProductLense reads paid invoices and payment intents to link subscription charges to their customers, and never needs write access.
- In ProductLense, open your site's Settings → Billing connections → Stripe, paste the key and click Connect and import history.
- Copy the webhook URL from the connection card. It has the form
https://api.productlense.app/api/webhooks/stripe/<connection-id>. - In Stripe, open Developers → Webhooks → Add endpoint, paste the URL and select the events below.
- Copy the endpoint's signing secret (
whsec_...) into the Stripe connection in ProductLense.
The events ProductLense uses, and what each one does:
| Event | What ProductLense does |
|---|---|
charge.succeeded | Records the charge with its fee |
charge.refunded | Records the refunds on the charge |
refund.created, refund.updated, charge.refund.updated | Records or updates the refund |
charge.dispute.created | Records a chargeback with the dispute fee |
customer.created, customer.updated | Updates the customer and its metadata |
customer.subscription.created, customer.subscription.updated, customer.subscription.deleted | Updates the subscription |
invoice.paid | Links the subscription to its charge |
Every delivery is verified with the Stripe-Signature header, a timestamp plus an HMAC-SHA256 with your signing secret, and deliveries older than 5 minutes are rejected.
On first connect, ProductLense backfills the last 12 months. After that, webhooks keep it current and a nightly reconcile at 03:00 UTC re-reads from the last sync minus 48 hours, so a missed webhook never leaves a gap.
What you see afterwards
Once charges flow in, the Sources table shows revenue per channel, source, medium and campaign, each row attributed by first touch. Revenue is charges minus refunds minus chargebacks, before fees. Only payments with status succeeded count, and every amount is converted to USD at the European Central Bank rate for the payment date.
Net revenue subtracts the fee Stripe reports on the balance transaction, balance_transaction.fee. Stripe reports it in your settlement currency, so ProductLense converts it to the charge currency with the balance transaction's exchange rate. It is exact, not estimated. For a charge, net is amount minus fee. For a refund or chargeback, net is minus the amount plus the fee: the refund's own balance transaction fee, or the dispute fee.
Refunds and disputes land on the same channel as the original charge, because they belong to the same customer and so the same first touch. A campaign that brings buyers who refund a month later shows it in its own row.
Add ad spend and the row turns into profit. The Profit page computes Profit = Revenue − Fees − Ad spend for the same key, with CAC, ROAS and payback beside it. Spend is entered by hand or as CSV with the same channel, source, medium and campaign as your UTM tags, as described in Add ad spend. If the four values differ by one character, spend and revenue show up as two rows. Profit per channel is on the Growth and Scale plans.
Revenue that cannot be linked to a visitor still counts toward totals, MRR and LTV, under an empty key. The revenue attribution feature page shows the result on demo data.
Questions founders ask
Does Stripe need to see my UTM parameters?
No. The tags are read by the ProductLense script on the landing page and stored on the visitor's first touch. The Stripe payment finds that first touch through the customer, by the user id in metadata or by email. Nothing about the visit has to be passed into Stripe or carried through its checkout.
What if the customer pays before my site calls identify?
The payment is stored without a channel. A daily job at 04:00 UTC takes up to 500 unattributed payments from the last 90 days and resolves them again. As soon as your site identifies the user, the next run credits the right channel. Payments older than 90 days with no link stay unattributed but keep counting toward totals.
Which attribution model does ProductLense use for Stripe revenue?
First touch only. The channel, source, medium, campaign, referrer domain and landing path of the customer's first recorded visit get credit for every payment that customer makes, including renewals. Each payment is attributed exactly once, so revenue by channel adds up to what Stripe paid you. There is no last-touch or multi-touch model.
Does ProductLense change anything in my Stripe account?
No. The connection uses a restricted key with read permissions only, and ProductLense only reads customers, subscriptions, charges, refunds, disputes, balance transactions, invoices, payment intents and checkout sessions. It never creates, changes, refunds or cancels anything, and it never moves money. The webhook endpoint receives events from Stripe and sends nothing back.
What if my Stripe customer has no user id in metadata?
ProductLense falls back to the customer email, matched to the email trait you sent with identify or to a user id that is that email. If the customer paid with a different address, or your site never sent the email trait, the payment stays unattributed and shows under an empty key. Set pl_user_id on new customers to close that gap.
Are refunds and disputes subtracted from the right channel?
Yes. A refund or chargeback belongs to the same Stripe customer as the charge, so it resolves to the same first touch and the same row. Its amount is subtracted from revenue, and the fee on the refund's balance transaction, or the dispute fee on a chargeback, is subtracted from net revenue. The backfill includes refunds and disputes too.