Skip to content

Revenue5 min read

Revenue attribution without cookies, explained

First touch without a cookie, a user ID at sign-up, a join on the server. The whole flow, step by step.

atrby teamProduct and engineering

The short version

  1. The script remembers how this visit started, for the life of the browser tab.
  2. At checkout, you pass the visit id to your payment processor.
  3. At sign-up, you pass your own user id to atrby.
  4. When the payment webhook arrives, atrby joins it to the visit on the server.

No step needs a cookie or local storage.

1. First touch, kept per tab

When the page loads, the script makes a random visit id and stores it in sessionStorage as atrby_sid. Next to it, it stores the first touch: the referrer, the landing page's query string and its path.

sessionStorage belongs to one tab and ends when that tab closes. It isn't sent with requests the way a cookie is, and it doesn't follow the visitor to another tab or another day.

Every event the script sends carries that first touch. So a visitor who lands from a newsletter, reads your pricing page, then browses the docs still carries the newsletter source on every event. The server reads the UTM tags from the stored query string and keeps only the host of the stored referrer.

2. Counting visitors on the server

Unique visitors are counted separately, on our server. The visitor id is a one-way hash of a random daily salt, your site, the IP address and the user agent.

  • The salt is made fresh each UTC day and the previous day's is deleted.
  • The IP address is used only for the hash and is never stored.
  • So the same person on two days counts as two visitors, and nobody, including us, can link the two.

That id is for counting. Attribution doesn't depend on it. The visit id and the user id do that work. More detail is in cookieless and cookie mode.

3. Pass the visit id at checkout

When the visitor starts a checkout, read the ids in the browser:

const ids = window.atrby?.ids();

That returns { atrby_sid, atrby_site }. Send it to your server with the checkout request and put it in the processor's metadata. On Stripe, set it on both metadata and subscription_data.metadata so renewals carry it too. Lemon Squeezy, Polar, Paddle, Dodo and Shopify each have their own field; the revenue docs list them.

Your processor keeps the ids on the payment. From each payment, atrby stores the amount, currency, the processor's payment id, the matched source, and the customer email and name the processor sends.

4. Identify at sign-up

The visit id only covers the visit where checkout starts. That's fine for someone who lands and buys in one sitting. It isn't enough for a SaaS trial, where a person lands on Monday, signs up, and pays two weeks later from a bookmark.

So when someone signs up, call:

window.atrby?.identify(user.id);

atrby stores your user id with the current visit, and nothing on the device. Then add the same id to the checkout metadata as atrby_uid, next to the visit ids.

Use a stable id such as your database id, up to 128 characters. Avoid email addresses: the id is stored as given. Call it once, at sign-up. If you also call it on every login, payments are credited to the most recent visit where you called it, which is usually not the one that brought the person in. See identify users.

5. The join on the server

When the processor sends a payment webhook, atrby checks its signature and reads the metadata. Then it looks for a source in this order:

Visit idIf atrby_sid matches a visit from the last 90 days, the payment is credited to that visit's source, landing page and campaign.
User idIf the visit is unknown or older than 90 days, atrby uses atrby_uid and credits the visit where you most recently called identify() for that user.
NeitherThe payment is still counted, labelled Unattributed. We never guess.

The 90-day fallback matters for renewals. A monthly subscription that started in January carries its January visit id in March, April and beyond. Once that visit ages out, the user id keeps the renewal credited to the right source.

Each payment is stored once per processor and payment id, so webhook retries don't double count. Refunds are subtracted from the source of the original payment.

Where this falls short

  • Anonymous journeys across days. If someone visits twice without signing up and then buys on the second visit, the payment goes to the second visit. Nothing links the first one.
  • Fresh tabs. The visit lives in one tab. A visitor who comes back in a new tab, or after closing the browser, starts a new visit.
  • Funnels are per visit. Funnel steps must happen in one visit, so multi-day journeys only show up to where the first visit stopped.

If you need to follow anonymous visitors over weeks, switch the site to cookie mode in settings. It keeps a first-party cookie for a year, and needs consent in the EU and UK. For most products with a sign-up step, the visit id plus identify covers the journeys that end in a payment.

Check it on your own site

  1. Open your site in a private window from a tagged link, such as one with utm_source=test.
  2. Sign up. Your visit in Sessions shows an identify step.
  3. Make a test purchase with both ids in the metadata. It appears on the Revenue screen against the source you arrived from.

Once payments carry a campaign, you can set them against ad spend. The ROAS per campaign guide covers the formula, and the Google Ads import guide covers the spend file.

Start your 14-day free trial