Guide
Stripe revenue attribution without code
Published · 6 min read
Stripe knows who paid, how much and when. Your analytics knows where visitors came from. The useful question sits between the two: which post, link or release led to this payment? Neither tool answers it alone, because neither knows about the other.
The textbook answer involves passing an identifier through your checkout and writing a webhook handler. It works, and many solo makers never get around to it. This guide explains what you can learn without touching your checkout, how reliable it is, and the small change that makes it exact when you want more.
Why the two halves do not meet
When a visitor lands on your site, your analytics records a visit with its source: a referrer, a tagged link. When that visitor pays, Stripe records a payment with an amount and a customer.
Nothing ties one record to the other. The visit lives on your site. The payment often happens on a page hosted by Stripe. The customer's email address is in Stripe and, quite rightly, not in your analytics.
So you end up with two true statements that do not combine: "Reddit sent 1,200 visits this week" and "Stripe took nine payments this week".
Three ways to connect them
There are three levels, from exact to approximate.
Exact: an identifier travels with the payment
Your site gives each visitor an identifier. When you create the payment, you attach that identifier to it. Later, the payment can be traced back to the visit, and so to its source.
Stripe has a field built for this kind of thing. Metadata lets you store your own key and value pairs on Stripe objects, for your own reference. Stripe's documentation gives the limits: up to 50 keys, each key up to 40 characters, each value up to 500 characters. It also warns never to put sensitive information such as card or bank details in it. A random visitor identifier is exactly the kind of harmless reference it is meant for.
This is the most reliable method. It requires a change where you create the checkout.
Inherited: the same customer, again
Once a customer's first payment has a source, their later payments can reuse it. A renewal in month four, or a second purchase, belongs to the channel that brought that customer in the first place. No visit is needed, and nothing has to change in your checkout.
Estimated: matched by time
With no identifier at all, one thing is still known: when the payment happened. If exactly one visitor was on your pricing or checkout page in the minutes before a payment, that visitor very probably made it.
This is the method that needs no code. It is also the one that can be wrong, and any honest tool should say so every time it uses it.
How upTake does it without code
You connect Stripe with a restricted, read-only key. In Stripe, you create it under Developers, API keys, and give it read access to charges, customers, checkout sessions and subscriptions, and nothing else. Stripe's documentation on API keys explains restricted keys. A read-only key cannot move money, refund a payment or change anything in your account.
From there, upTake reads your successful payments, going back 90 days on the first sync, and tries each of the three levels in order:
- If the payment, or its Checkout session, carries the visitor identifier in its metadata, the match is exact.
- Otherwise, if the same Stripe customer already has a payment with a source, the new payment takes the same one. Renewals and repeat purchases are handled this way.
- Otherwise, upTake looks for the visitor whose last page view came within 30 minutes before the payment. If two or three visitors fit, it takes the one closest in time. If more than three were there, it does not guess. A match made this way is marked estimated.
A payment that fits none of the three is still counted in your revenue. It is shown as not traced to a source.
After each sync, upTake tells you the split: how many payments were matched to a visitor, how many were matched by time, and how many are untraced. On the dashboard, any revenue attributed by time carries an "estimated" label, and the share of your revenue traced to a source is written next to the total. The Stripe integration page lists exactly what is read.
How far to trust matching by time
Matching by time is a reasonable guess, and how reasonable depends on your traffic.
- With few visitors at a time, it is usually right. If one person was on your pricing page at 14:02 and a payment arrived at 14:06, there is little doubt.
- With many visitors at once, it stops being a match and becomes a draw. During a launch, with fifty people on the site, the closest visitor in time is one guess among many. That is why upTake leaves the payment untraced when more than three visitors were present.
- With delayed payments, there is nothing to match. Someone who visits on Monday and pays on Thursday from an emailed invoice has no visit within 30 minutes. That payment stays untraced, which is the correct outcome: better no source than a wrong one.
- With renewals, there is no visit at all, and none is needed. A subscription renewing in its fourth month keeps the source of its first payment. If that first match was an estimate, the renewals are estimates too.
Use estimated numbers for what they are good at: large differences. If Reddit shows 400 dollars and LinkedIn shows nothing, the conclusion survives a few wrong matches. If two channels are 20 dollars apart, the estimate cannot tell you which one won.
When one line is worth it
If attribution matters to your decisions, make it exact. The change is small.
The upTake script exposes the visitor identifier on your site:
const visitor = window.uptake.visitorId();
Send that value to your server when you create the Checkout session, and attach it as metadata under the key uptake_visitor:
const session = await stripe.checkout.sessions.create({
// your existing options
metadata: { uptake_visitor: visitor },
});
That is the whole change. Every payment made through that session is then matched exactly. If you use Payment Links, no server code is involved; Stripe's documentation on metadata describes how values set on a link are copied to the sessions it creates.
Do it when:
- your traffic is high enough that several visitors are often on the site at once,
- you are about to spend money or weeks on a channel and want the numbers behind the decision to be solid,
- a large share of your payments shows as estimated and that bothers you.
Skip it while you are finding your first customers. Estimated attribution on ten payments tells you roughly the same thing as exact attribution on ten payments.
What attribution will not tell you
- Why they bought. It gives you the door they came through, not the reason. Ask a few customers; it takes one email.
- The whole path. Someone may have seen you three times in three places. upTake credits the first source that brought the visitor, over a 90-day window. Other tools choose the last one. Neither is the full story.
- People who never clicked. A reader who saw your post and typed your name into a search engine two days later arrives as search. See the guide on UTM links for indie hackers for what tagged links can and cannot catch.
The short version
- Connect Stripe with a restricted, read-only key. No change to your checkout.
- Read the split after the first sync: matched to a visitor, matched by time, untraced.
- Trust estimated numbers for large differences, not for close calls.
- When it starts to matter, pass the visitor identifier in the Checkout metadata and make it exact.
Knowing roughly which channel pays, this week, is worth more than a perfect setup you will build next quarter.
See this in upTake
Home shows traced revenue and the share credited to a source. Read the method before treating it as evidence: matches by time are estimates and some payments remain untraced.