Tracking & attribution

How a sale is followed from click to commission.

An affiliate sale is not tracked by magic. It is tracked by a click ID, a cookie and a server that remembers exactly what was agreed in the second someone clicked. Here is the whole chain – including the places where it does not hold. You will find those yourself the first time a sale does not land.

The chain

Seven steps from click to approved sale.

A tracking link is not just a link with a name on it. It is a redirect through the network’s server, and that is where the whole agreement is written down. Run through the process below – each step shows both what happens and what the system stores at that moment.

the program’s rate today

12.00%

unchanged so far

the rate on your click

–

not locked yet

cookie window

–

starts at the click

commission

–

no sale yet

step 01 / 07

Step 01 · shared

The affiliate shares their link

app.affilyflow.com/go/k7m2qa

The link points at our server, not straight at the store. Until someone clicks, there is no row anywhere. A link is just a link.

What the system stores · nothing yet

rows written
0

There is nothing to count until someone has clicked.

Example. Click ID, order, dates and rates are made up to show the mechanism – it is a calculation, not a promise of earnings. The cookie window differs from program to program; here 30 days are used.

The whole attribution process in text

  1. shared: The affiliate shares their link. The link points at our server, not straight at the store. Until someone clicks, there is no row anywhere. A link is just a link. Stored in nothing yet: rows written = 0. There is nothing to count until someone has clicked.
  2. t + 0 ms: The click is recorded on the server – and the rate is locked. The visitor hits /go before the store. A known crawler is logged separately as bot traffic, but gets no click row or attribution. Otherwise the link is looked up, and a click row is written. Four fields are copied from the program onto the row. From this millisecond they belong to the click – not to the program. Stored in clicks: click_id = c_9fK2xQ1bR7dLm; affiliate_id = a_4820; program_id = p_117; commission_rate = 12.00%; commission_type = percent; cookie_days = 30; recurring_months = 0; created_at = 2026-03-04T09:12:41Z. The four marked fields are the rate lock. The window is measured from this row – on our server, so the client cannot lie about the click’s age.
  3. t + 40 ms: The visitor lands at the store with ?afid=. A 302 redirect sends the visitor on with the click ID in the URL. The store’s script puts the ID in a first-party cookie on the store’s own domain and in localStorage. Both are renewed on return visits, so a cleared store can restore itself from the other. Stored in the browser: cookie af_click = c_9fK2xQ1bR7dLm; cookie lifetime = 90 days; localStorage = the same click ID; domain = the store’s own (first party). The client storage is a copy. The original – and the window – is on the click row.
  4. day 12: The store lowers the rate – and it does not affect your click. Twelve days into the window, the store changes the program’s rate from 12.00% to 8.00%. The click row is unchanged. New clicks from tomorrow get the new rate; the click from day 0 keeps its own. The conversion is calculated on the click’s copy, not on the program as it looks today. Stored in programs → clicks: programs.commission_rate = 12.00% → 8.00%; clicks.commission_rate = 12.00% · unchanged; clicks.cookie_days = 30 · unchanged; days left of the window = 18. This is where the whole difference lies: the rate lives on the click, not on the program.
  5. day 21: The purchase happens – the order is reported. Nine days later the visitor buys for DKK 1,495.00. The order confirmation page’s script or the platform’s integration reports the order with the click ID. The same order cannot count twice – not even if the customer reloads the order confirmation page. Stored in incoming event: click_id = c_9fK2xQ1bR7dLm; external_order_id = #10442; order_value = 1,495.00; currency = DKK. The event is logged whatever it ends up becoming. Including the reports that do not become a sale.
  6. day 21: record_conversion matches the click and calculates on the locked rate. The server looks up the click ID, measures the click’s age against the window on the click (21 ≤ 30), checks that the order ID has not been seen before on the program, and multiplies the order value by the rate from the click row. The sale is created as pending – a conversion never credits a balance automatically. Stored in conversions: click_id = c_9fK2xQ1bR7dLm; age vs. window = 21 / 30 days; rate used = 12.00% · from the click; order_value = 1,495.00; commission = 179.40; status = pending. With the store’s new rate, the amount would have been DKK 119.60. The difference of DKK 59.80 is the rate lock, counted in kroner.
  7. day 51: The store has 30–90 days to reject – otherwise the sale is approved. The store can reject within the program’s approval period – 30, 45, 60 or 90 days – and a rejection requires a reason. If nothing happens, a cron job sets the status to approved. Approved is not the same as paid out: payout is a separate step, and the amount is decided by the server based on approved sales. Stored in conversions: status = pending → approved; decided by = cron/auto-approve; days the store had = 30; commission = 179.40. A rejection without a reason is not possible – the field is required all the way down in the database.

Notice day 12: the store lowers the rate, and the two lines split. The click that has already been driven keeps its own rate. Which four fields are locked is explained in the rate lock section.

The client side

Cookies in 2026 – what really survives.

The attribution here never rests on a third-party cookie. The click ID comes first in the URL, and then becomes a first-party cookie on the store’s own domain. There is no third party to block.

Safari’s tracking protection caps cookies set from JavaScript at around 7 days. That is why the script writes the click ID to both a cookie and localStorage and renews both on every return visit. If the cookie is deleted, it is restored from localStorage.

The renewal does not extend the attribution. It only keeps the ID alive long enough for the server to make the decision. The window itself is measured from the click’s own timestamp, which the browser is never allowed to touch.

What is stored where

URL parameter
?afid=<click-id> · set by the redirect
First-party cookie
af_click · the store’s own domain
localStorage
af_click · survives cookie clearing
The server
click time, window, rate – the source of truth

If the browser is cleared completely, or the device changes, the attribution is gone. There is no hidden way round it.

Click recording

The click sits in the road, not beside it.

A tracking pixel is a voluntary request from the browser. It can be blocked by an extension, dropped by tracking protection or never get sent at all, because the user navigates on first. Every lost pixel call is a lost click in the statistics – and a click that can never carry a sale.

Our click is not a request on the side. It is the road itself. The browser has to pass through our server to reach the store, and the click row is written before the redirect. There is no extra call to block: a block that stops the redirect also stops the visitor from getting there – and then there is no sale to track.

The same goes for the clicks we do not want. Known crawlers and link previews – Facebook, Slack, curl, uptime services – are still sent on, so a shared link never looks dead. But they get no click recording and no click ID. Bots are not counted, so they can neither pollute EPC or the conversion rate nor take an attribution. Bot traffic is logged separately and can be seen on the store’s Analytics. If an affiliate is suspended, the same happens: the visitor lands at the store, but the click is not recorded.

The attribution window

The window is decided on the server.

The store chooses 14, 30, 45, 60 or 90 days. The default window is 30. The number is copied onto the click row at the moment of the click.

At conversion, the server looks up the click by its ID, fetches the click’s own timestamp and compares it with the window stored on the click – not with the program as it looks today. If the click is too old, expired is returned and no credit is given.

The point is who decides. The browser sends an ID, not a timestamp. So the client cannot lie about the click’s age – neither to extend an expired window nor to sneak a conversion in. That is also what makes cookie stuffing pointless: a cookie without a recorded click behind it is nothing.

An honest caveat: there is an older fallback route where a conversion is reported with the affiliate’s tracking ID and the store’s slug instead of a click ID. That route does not know the click’s age and therefore cannot enforce the window. It is deliberately more forgiving – and it is the reason we do not claim to catch every single sale.

The model

One click ID. One model.

There is exactly one attribution model: last click, decided by the click ID the conversion carries. No splitting between several affiliates, no multi-touch weighting, no view-through credit for views without a click.

One model is easier to check than five. Both parties can point to the same click and agree on what happened.

Rate lock

The commission is locked the moment someone clicks.

The cookie window is not the only thing that is frozen. Four fields are copied from the program onto the click row, and they are what the conversion is calculated on:

  • commission_rate

    The rate. If the store lowers it tomorrow, it does not affect traffic you have already sent.

  • commission_type

    Percentage or fixed amount. It cannot be switched under the feet of a click you have driven.

  • cookie_days

    The window, measured from this click. Independent of later program changes.

  • recurring_months

    The number of months on subscriptions. An ongoing agreement cannot be cut back afterwards.

There is a related thing worth knowing when you read a commission rate with us: the affiliate gets exactly the rate the program shows. The network fee is part of the store’s total price per sale – it is not deducted from the affiliate’s commission. So the rate on the program page is the rate that is paid out. See the rates in the program catalogue.

At checkout

What happens on each platform.

Every route ends in the same place: a function in the database that looks up the click, enforces the window, checks for duplicates and creates the sale as pending. It is the way there that differs.

Shopify

Web Pixel, no code in the theme.

A Web Pixel in the customer’s browser reads the click ID and reports the order with the order ID. Commission is calculated on the value of the goods without shipping, where it is not yet confirmed whether the VAT has been deducted. If the order is refunded, the store has to reject the sale itself within the approval period.

Stripe

Signed webhook, no API key.

We never ask for your Stripe API key – only a webhook signing secret, which can only verify the sender. Commission is calculated excluding VAT, and if the payment is refunded or disputed, it is rejected automatically – with a partial refund, it is reduced proportionally. For subscriptions, renewals are credited too, for the number of cycles that was locked on the click.

Your own website

A script, one line on the order confirmation page.

The script sits site-wide and stores the click ID. The order confirmation page calls a function with order number and amount. The report is sent with sendBeacon, so it survives the page navigating on immediately afterwards – for example to a thank-you page.

The Stripe webhook and the WooCommerce plugin are server-to-server tracking in the true sense of the word: the store’s server talks to ours, without going through a browser. That is why they are out of an ad blocker’s reach – and why they are the route we build first, wherever the platform allows it. Two honest caveats: the Shopify app reports the sale with a Web Pixel in the browser, not server to server. And automatic rejection on refund exists on WooCommerce and Stripe, where a partial refund reduces the commission proportionally – Shopify refunds and site-script sales must be rejected manually by the store within the approval period.

The filters

What is not counted.

Every reported event gets a result status. These are the actual names from the log – the ones the store can see for itself when a sale did not land as expected.

  • duplicate

    The same order came in twice

    Deduplication happens on order ID per program. If the customer reloads the order confirmation page, or Stripe sends the same webhook again, the extra report is logged here. The order is counted once.

  • expired

    The click was too old

    The conversion came after the window stored on the click. No credit. The window is read from the click, not from the program as it looks today.

  • blocked

    The store has blocked the affiliate

    Blocked by a trigger in the database, not by an if statement in an API route. Every insertion route is closed – including the ones built tomorrow.

  • store_mismatch

    The webhook pointed at another store’s program

    A store’s webhook can only credit its own store’s program. The attempt is logged and rejected.

  • not_recurring

    Renewal on a program without recurring commission

    The subscription renews, but the program only pays for the first sale. The event is logged, so the store can see why no commission came.

  • no_affiliate

    There was no click ID on the event

    An entirely ordinary sale with no affiliate behind it. It is logged too – otherwise you cannot tell "no affiliate" apart from "tracking broke".

The detail that costs the most if it is missing

If the order ID is still the placeholder from the installation guide – YOUR_ORDER_ID, {{order_id}} and the like – we treat it as no ID rather than a real one. Otherwise the first sale would claim the ID, and every single sale after that would be rejected as a duplicate. Forever, and completely silently. It is better to risk a double count the store can reject than a silent loss of every sale.

Placements

Tracking per page – without tracking visitors.

By default, browsers only send the domain as the referrer, not the path. So we always see the site – it shows up by itself in the affiliate’s dashboard – but the question “which of my pages created that sale?” needs a little more.

One solution is the link itself: our HTML code has referrerpolicy="no-referrer-when-downgrade", which asks the browser to send the full page address along (Chrome, Edge and Firefox do). The other is a script on the affiliate’s own site, which also covers Safari and apps. It adds the current page’s URL to every Affilyflow link on the page – including links inserted later by infinite scroll, SPA navigation or pop-ups. The URL is normalised to domain plus path, so query and hash do not split a page into many rows. The click then carries its placement with it all the way to the conversion.

The script sets no cookies, tracks no visitors and does not phone home. It writes a parameter on a link, and that is all. Combined with sub-ID tracking and deep links it gives earnings per URL rather than per account.

The dashboard shows what share of the clicks have a known page, and highlights it when coverage drops below 80%. A number you cannot trust is worse than no number – so we show how much of the picture is missing.

Monitoring

We also log what never became a sale.

Every single tracking event is written to an event log: source, order ID, click ID, amount, VAT, discount – and our result status. Including events that did not become a conversion.

The two things are deliberately kept apart: the conversions table only contains the money truth, the event log is the raw diagnostic trail. Without the log you cannot tell the difference between “no sales came” and “the sales came, but were rejected for a reason nobody noticed”. The store can read its own events.

Heartbeat · every 6 hours

We monitor ourselves whether your tracking script is down, and tell you. Every 6 hours we fetch the front page of every active store and check whether the script is still there.

If the status goes from live to down, we send an email. One – not one every six hours. And if the site does not respond at all, we only note that we looked: a network error should not be called an outage.

The limits

What we do not do.

The following are not gaps we are about to close. They are choices. And it is the part of this page we would most like to be quoted on.

No fingerprinting

We build no device profile from screen size, fonts and time zone to recognise a browser that does not want to be recognised. What fingerprinting is.

No cross-device

If someone clicks on their phone and buys on their laptop, the sale is not attributed. There is no identity graph tying the two together.

No cookieless attribution

Attribution rests on a click ID in the URL plus client storage. If someone sells you cookieless tracking in 2026, ask what is under the bonnet. What cookieless tracking is.

No claim of 100%

The fallback route does not know the click’s age and cannot enforce the window. Client storage gets cleared. Some sales do not land – and a network that claims otherwise has not read its own code.

Questions

What people actually ask.

  • How long does attribution last after a click?

    The store chooses 14, 30, 45, 60 or 90 days. The number is copied onto the click itself the moment someone clicks, and the server measures the conversion against that number. If the store changes the window tomorrow, it does not affect the clicks you have already sent.

  • What happens if the customer clears their cookies?

    The click ID is stored both in a first-party cookie and in localStorage, and both are renewed on return visits – so a deleted cookie can be restored from the other store. If both are gone, the attribution is gone. We have no fingerprinting fallback that guesses who the person was.

  • Does tracking work with ad blockers turned on?

    The click does, always: it is a redirect through our server that the browser has to pass through to reach the store. The conversion depends on the route. Stripe sales are reported server to server via a signed webhook, and the WooCommerce plugin reports from the store’s server – neither of them touches the browser. The Shopify app’s Web Pixel and the site script, on the other hand, run in the browser and can be blocked – which is why we build the server route first wherever the platform allows it.

  • Can the store lower my commission after I have sent the traffic?

    No. The rate, commission type, cookie days and number of recurring months are stored on the click row and used at conversion. An ongoing recurring agreement cannot be cut back afterwards either. And the affiliate gets exactly the rate the program shows – the network fee is part of the store’s total price and is not deducted from the commission.

  • Does the same order count twice if it is reported again?

    No. Deduplication happens on order ID per program, and Shopify’s two formats (gid://shopify/Order/123 and 123) are normalised to the same ID. If the customer reloads the order confirmation page, or Stripe sends the same webhook again, it does not become a new sale. The extra event is logged as duplicate, so you can see that the order came in again.

  • Do you track across devices?

    No. If someone clicks on their phone and buys on their laptop, the sale is not attributed. We could guess with fingerprinting or an identity graph – we do not. That also means we cannot promise to catch every single sale, and we would rather say so out loud than promise the opposite.

Get started

Tracking you can verify.

Stores install a script or connect their platform. Affiliates get a link that works from the first click. Both sides can see exactly what was recorded.