Skip to content
DocsHow attribution works

Get started

How attribution works

First touch, identity stitching, measured and modeled numbers, the attributed share, and why Joinsight never reaches 100%.

Stripe knows who pays you. Search Console knows which queries sent clicks to which pages. Neither knows that the customer who started paying last Tuesday first arrived on a blog post from a Google search. Joinsight connects the two by carrying one fact across the gap: where this person first landed, attached to the customer record that pays.

This page explains how that link is made, which numbers are measured and which are modeled, how attributed revenue is counted, and why the attributed share never reaches 100%. The exact wording of every metric is on metric definitions.

The chain from visit to revenue

Attribution is a chain of four links. Each one has to hold for a customer to be credited.

  1. A visit. The snippet records the visitor's first touch under an anonymous ID.
  2. A signup. Your identify() call links that anonymous ID to your user ID.
  3. A payment. Stripe or RevenueCat carries the same user ID, or the same email.
  4. A credit. The customer's MRR at first payment is credited to the first touch's page and channel.

If any link is missing, the customer still counts in MRR and every revenue metric. They just show as unattributed.

The first touch

The snippet sends one event per browser session, from the page the visitor landed on: when the session starts, or when consent is given on that page. A page reached through a link on your own site is never treated as a landing page. The event carries:

  • the landing path, lowercased, without the query string or a trailing slash;
  • the referring domain, when there is one;
  • UTM source, medium, campaign, content and term, when present;
  • an ad click ID, when present: gclid, gbraid, wbraid, msclkid or fbclid.

The first event for an anonymous ID becomes that visitor's first touch and is never overwritten. Later sessions still count as sessions on the page they landed on, but they don't change the credit. Nothing is stored before consent: the anonymous ID is written to the browser's local storage only once consent is given. Installation and consent are on Install the tracking snippet.

Each first touch is classified into a channel. The first rule that matches wins: a Google or Microsoft Ads click ID as paid search, unless the UTM tags already say paid; then UTM source and medium; then fbclid as social; then the referrer domain (AI assistants such as ChatGPT as AI search, webmail as email, search engines as organic search, known social domains as social); and otherwise direct. The rules, and how to tag links so social traffic isn't counted as direct, are on Channels, UTM tags and campaign links.

identify() at signup

When someone signs up, your app calls identify() with your user ID and, optionally, the email. That stores a link between the browser's anonymous ID and your user ID. The email is hashed on arrival with a keyed hash; the plain address isn't kept.

If the same user ID ends up linked to several anonymous IDs, for example because identify() ran on two devices, the earliest of those visits is the first touch. Details, including the server-side call, are on Link visitors to customers with identify().

Matching billing customers

Billing records are matched to people in this order:

  1. User ID. For Stripe, metadata.user_id on the customer, or on the subscription when the customer has none. For RevenueCat, the app user ID. RevenueCat's anonymous IDs, the ones starting with $RCAnonymousID:, are ignored.
  2. Hashed email. When no user ID matches, the email on the billing record (the Stripe customer's email, or RevenueCat's $email attribute) is hashed the same way and compared with the emails passed to identify() and with other billing records. Two records with different known user IDs are never merged on email.

Matching isn't settled once. When a user ID reaches a billing record later, for example metadata.user_id added to an existing Stripe customer, the next recompute links the record to the customer with that user ID, merging the two when the record had only been matched on email.

The same rules make one person one customer across Stripe and RevenueCat: someone paying on the web and in your app counts once, with their MRR summed. Email matching is a fallback, not a plan. It fails whenever someone signs up with one address and pays with another, which is common with work and personal emails. Setting the user ID at Checkout is the reliable path, explained in Stripe Checkout metadata and the user ID.

Crediting the customer

Only a visit on or before the day of the customer's first payment can be their first touch. A visit linked afterwards, for example when a customer from before the snippet logs in and identify() runs, didn't bring them, so it is never credited.

Once a customer is linked to a first touch, their revenue is credited to that touch's landing page, its channel, and, for Instagram, the post whose ID is in the link's utm_content. Sessions are counted on the page each session landed on, and signups on the landing page of the visitor's first touch. Together with Search Console clicks, that is what lets the dashboard show each page's path from clicks to sessions, signups, paying customers and MRR.

Why first touch

Joinsight uses one model: first touch. The whole credit for a customer goes to the first visit Joinsight recorded.

The products Joinsight is built for are rarely bought on the first visit. People read a post, leave, come back weeks later by typing your domain, and then pay. Under last touch, that final visit gets the credit: direct traffic, a branded search, or your own onboarding email. All three are effects of earlier marketing. First touch puts the credit where the introduction happened, which is the number that tells you what to write next.

Three things to keep in mind:

  • It over-credits discovery. A pricing or comparison page that convinces people who already knew you looks weak under first touch.
  • "First" means the first visit Joinsight saw, not necessarily the first that happened.
  • It doesn't drift. A customer credited to a post in March is still credited to it in December, which keeps past months comparable.

For paid campaigns, where you want to know whether the last click converted, read your ad platform's own reports next to Joinsight. The longer argument, with the cases where last touch is the better lens, is in First touch vs last touch attribution.

Measured and modeled

Every attributed number on screen carries a label: Measured or Modeled. The difference is whether a real person was linked to the thing that gets the credit.

Measured: pages, channels and posts

Page revenue, channel revenue and post revenue are measured. Each paying customer was linked, through the chain above, to a specific landing page, channel and link. Nothing is estimated. Measured is not the same as complete, though: it covers the customers Joinsight could link, which is what the attributed share tells you.

Modeled: keywords

Keyword revenue is modeled, always. Google never reveals which query a specific visitor typed. Search Console reports clicks per query and page in aggregate, and the snippet sees a visitor who arrived on a page from google.com, with no query attached.

So Joinsight splits each page's organic Google revenue across the queries that sent the page clicks, by each query's share of the page's clicks. The split runs month by month: a customer's revenue is split using the clicks from the month of their first visit.

Two buckets keep the split honest:

  • Hidden queries. Google anonymizes rare queries. Their clicks appear in the page's totals but not in the query list. The revenue those clicks account for stays in a separate hidden-queries bucket instead of being spread across the visible queries, which would inflate them.
  • Without search data. Organic Google revenue on pages that had no Search Console clicks that month can't be split at all, so it is shown on its own.

Only organic Google visitors are split by query. Revenue from Bing, social or direct traffic is attributed by page and channel only. Treat keyword revenue as directional: good for deciding which topics to write more about, not for exact accounting. Why hidden queries can be large is covered in Search Console hidden queries.

How attributed MRR is counted

Attributed MRR is grouped by the month of each customer's first payment, using the customer's MRR at that first payment.

That choice keeps history still. If a customer who started paying in March upgrades in May, March's attributed MRR doesn't change. The upgrade shows in May as expansion in MRR movements. Customer pages show both figures: current MRR and MRR at first payment.

It also means attributed MRR answers "how much new recurring revenue did this page bring in this period", not "how much revenue does this page's audience pay today". For that, open the customers credited to the page.

One-time sales, such as Payment Links or lifetime deals, are reported as one-time revenue and are not part of attributed MRR. MRR itself is defined on metric definitions: normalized to a month, after discounts, excluding tax, trials and one-time payments.

A worked example

This is a made-up example to show the arithmetic. The numbers are not from a real workspace.

In March, 10 customers made their first payment. Their MRR at first payment adds up to €470.

Seven of them were linked to a first touch:

Landing pageChannelCustomersNew MRR
/blog/stripe-vs-paddleOrganic search · Google4 at €49€196
/pricing-guideEmail2 at €19€38
/Direct1 at €99€99

Attributed new MRR is €333. The other three customers, €137 together, have no first touch: one blocked the script, one refused consent and signed up two pages after landing, and one paid with a different email than they signed up with, with no user ID on the subscription. The attributed share for March is €333 ÷ €470, about 71%.

Now the keyword split for /blog/stripe-vs-paddle. All four customers first visited it in March, when Search Console reported 400 clicks to the page:

QueryClicksShare of page clicksModeled MRR
stripe vs paddle20050%€98.00
paddle vs stripe fees8020%€39.20
paddle merchant of record4010%€19.60
Hidden queries8020%€39.20

The €196 for the page is measured. The four query rows are modeled, and together they add back up to €196.

In May, one of the four upgrades from €49 to €99. March still shows €196 for the page. May shows €50 of expansion, and that customer's page shows €99 current MRR and €49 at first payment.

The attributed share

The attributed share is attributed new MRR divided by all new MRR, for customers whose first payment falls in the period. It sits next to every attribution report on purpose: a ranking of pages built on 20% of your new revenue is mostly noise, and one built on 80% is something to act on.

It stays below 100%, and that is expected. The usual reasons:

  • Blocked scripts. Some ad blockers drop analytics scripts. Serving the snippet from your own domain recovers most of them; see Install the tracking snippet.
  • Refused consent. Visitors who decline are never tracked across pages. They are only credited through the in-memory fallback described in the next section, which applies when they sign up on the page they landed on.
  • Switched devices. A visit on a phone and a signup on a laptop are two anonymous IDs, and only the laptop's is linked through identify(). The customer is credited to the laptop's first visit, often Direct, or stays unattributed when the laptop recorded none.
  • Cleared storage. The anonymous ID lives in local storage. Clearing it makes a returning visitor look new, with a later first touch, or none.
  • Customers from before the snippet. Customers who signed up before the snippet and identify() were live have no visit to link. This history can't be recovered: a visit linked at a later login came after they paid, so it doesn't count.
  • Missing links in the chain. identify() not called on some signup path, or a payment with neither the user ID nor the signup email.

Blocked scripts and missing links are the ones you can do most about. If the share is low, Troubleshooting walks through the checks. The overview also states it in customers: "7 of 10 new customers linked to a first visit".

Without consent, the snippet writes nothing to the device and sends no session event. On the page where the visitor arrived, it keeps the first touch (path, referrer, UTMs and click ID) in memory only. If the visitor signs up and identify() runs before they leave that page, the in-memory touch travels with the identify() call and becomes the customer's first touch, unless a consented visit is linked too.

That covers signups that happen without a full page load after landing: single-page apps, and signup forms on the landing page itself. On a site where the visitor moves through full page loads before signing up, the signup page was reached from your own site and has no landing details, so no touch is sent and the customer stays unattributed. Their real landing page is unknown, and an unknown first touch is shown as a gap in the attributed share rather than guessed. These visitors never appear in session counts, which is why revenue per visitor counts consented visitors only.

Everything is recomputed

Joinsight stores the raw material: subscription periods, payments, snippet visits and identify() calls. The numbers you see are derived from it, not accumulated over time. A recompute:

  1. links new billing records to customers, by user ID and then hashed email, and relinks records that have gained a user ID since;
  2. rebuilds daily MRR for every customer from the stored subscription periods, converted at the ECB rate of the day each billing period started;
  3. rebuilds one-time sales, net of refunds, at the rate of the day paid;
  4. rebuilds every customer's first touch from the stored visits and identify() calls, keeping only touches from on or before the first payment, and classifies it with the current channel rules.

It runs after a sync brings new billing data, about two minutes after an identify() call, and after you change the reporting currency. A request that arrives while a recompute is running queues one more run, so nothing that arrives mid-run is missed. Because it rebuilds rather than patches, an identify() call that arrives after the payment still credits the customer with the visit that came before it, in the month of their first payment, and a corrected definition applies to all of history with a re-run rather than a migration.