track.js
Complete reference for the tracking script – attributes, JavaScript API, what is stored in the browser, consent and errors.
track.js is Affilyflow's tracking script for webshops and websites. It stores the click when a customer lands from an affiliate link and reports the sale when the customer reaches the receipt.
Add the script
Add the line to <head> on every page – including the receipt page.
<script src="https://app.affilyflow.com/track.js" data-store="DIN_BUTIK" async></script>DIN_BUTIK is your store ID. The exact line with your ID is under Setup in the store dashboard – copy it from there.
Use async: the script must never hold the page back. The exception is the no-code lead trigger (data-lead-url), where the line must be without async – see Leads without code.
Attributes
| Attribute | Required | Meaning |
|---|---|---|
data-store | Yes | Your store ID. Sent with every sale and must match the click's store, otherwise the sale is rejected as store_mismatch. |
data-consent="required" | No | Nothing is stored in the browser until the visitor has consented to marketing. See Consent. |
data-lead-url | No | The thank-you page of a lead program. When the page matches, a lead is reported automatically. See Leads without code. |
data-cookie-domain | No | The domain the cookie is set on (e.g. shop.com). Without it, the script picks the broadest domain the browser allows. |
data-sale="ga4" | No | Read the purchase from Google Analytics 4 (dataLayer or gtag). See Google Analytics. |
data-sale-vat | No | Only with data-sale="ga4": the VAT percentage included in the Google Analytics amounts (e.g. 25). |
data-sale-shipping | No | Only with data-sale="ga4": incl if shipping is always included in value, excl if it never is. Without it, the script guesses. |
What the script stores
When a customer lands with ?afid= in the URL, the script stores the click ID both as a cookie and in localStorage, so it survives either one being cleared.
| Key | Content |
|---|---|
af_click | The click ID from ?afid= |
af_store | The store ID from data-store |
af_aff | Only for legacy direct links (?aff= + ?store=) |
- Lifetime: at most 90 days from the click. The cookie's
max-ageis always what is left of the 90 days. - Return visits never extend the window. On every visit the cookie and localStorage are written again with the remaining time, so a deleted cookie is restored from localStorage – but the timestamp is always the click's.
- localStorage stores the value as
{ v, t }– the value and the time of the click. A value older than 90 days is deleted when read. - The cookie is set with
path=/andSameSite=Lax, and withSecureon https. - Domain: the cookie is set on the broadest domain the browser allows (
shop.comrather thanwww.shop.com), so the click follows the customer to a receipt on another subdomain. The script finds it with a short-lived test cookie (_af_d) that is deleted immediately. On an IP address orlocalhostthe exact host is used. - Memory: the click ID from the URL is also kept in the page's memory, so a sale on the same page works even when storage is blocked (e.g. a private window).
Only Affilyflow click IDs
Only an afid that looks like ours (c_ followed by 4–64 characters: letters, digits, _ and -) is stored. If the store also uses another network with a parameter called afid, the foreign value can never overwrite an Affilyflow click.
affilyflow.conversion()
Report a sale from JavaScript. The call only works once track.js is loaded on the page.
affilyflow.conversion({ order: '1008', total: 499, currency: 'DKK' });| Field | Type | Meaning |
|---|---|---|
order | text | The order number. Used to stop duplicates. |
total | number or text | The amount commission is calculated on: goods after discount, excluding VAT and shipping. Text such as "1.249,00" is parsed, see Amounts as text. |
currency | text | Currency code, e.g. DKK or EUR. Upper-cased. Default DKK. |
store | text | The store ID. Only used if the browser doesn't already have it from data-store. |
How the call behaves:
- Without an affiliate click in the browser nothing is sent – and no error is logged. A normal purchase without an affiliate is not an error.
- The sale is sent with
navigator.sendBeacon, so it arrives even if the page navigates away right after. Without sendBeacon an invisible image request is used. - With
data-consent="required"the call waits until consent is settled and is sent only then. - For a lead program:
affilyflow.conversion({ order: 'lead_' + Date.now() })– no amount.
<script>
(function () {
var tries = 0;
(function send() {
if (!window.affilyflow) { if (tries++ < 50) setTimeout(send, 200); return; }
affilyflow.conversion({ order: '1008', total: 499, currency: 'DKK' });
})();
})();
</script>Amounts as text
A number is used as is. Text that is a valid machine number is also used as is. Other text is cleaned: everything except digits, dot, comma and minus is removed, and the last separator is the decimal mark. If there is only one separator with exactly three digits after it, it is a thousands separator.
Sent as total | Read as |
|---|---|
499 | 499 |
"1249.5" | 1249.5 |
"249.125" | 249.125 |
"1.249,00" | 1249 |
"1.249,-" | 1249 |
"499,00 kr." | 499 |
"kr. 499" | 499 |
"1,234.56" | 1234.56 |
Rejected placeholders
If the template's placeholder is still in order, the sale is sent without an order number – a placeholder must never take up an order number, or every later sale would be rejected as a duplicate. These values count as placeholders (case-insensitive):
DIN_ORDRE_ID,DIT_ORDRE_ID,ORDRE_ID,ORDER_ID,YOUR_ORDER_ID,ORDERIDXX,XXXetc. (two or more X)- Anything in
{{ … }},[ … ],< … >or% … %– a template variable that wasn't filled in
The server applies the same rule.
Duplicate guard in the browser
The server never counts the same order number twice in the same program. The browser also remembers what it has sent, so a reloaded receipt doesn't send again:
- With an order number: kept in localStorage (
af_sent) for 30 days. - Without an order number: kept in sessionStorage (
af_sent_s) as page and amount – the same receipt with the same amount is sent once per tab.
affilyflow.consent()
The store's own consent solution can control the script directly. Only relevant with data-consent="required".
affilyflow.consent(true); // store the click from the URL and send waiting sales
affilyflow.consent(false); // delete af_click, af_store, af_aff and af_sentIf the visitor says yes again on the same page, the click from the URL is stored again.
Consent
With data-consent="required" the script stores nothing in the browser – no cookie, no localStorage or sessionStorage, no test cookie – until marketing consent is given. Until then the click only lives in the page's memory and is not used. Without the attribute the script behaves as before.
<script src="https://app.affilyflow.com/track.js" data-store="DIN_BUTIK" data-consent="required" async></script>The script reads consent itself from:
| Source | What counts as yes |
|---|---|
affilyflow.consent(true) | The call |
window.affilyflowConsent | The value true |
| Shoporama's banner | shoporamaConsent.isAllowed('marketing') |
| Cookie Information | The category cookie_cat_marketing |
| Cookiebot | Cookiebot.consent.marketing |
| IAB TCF v2 | Publisher consent to purposes 1 and 7 (or GDPR doesn't apply) |
- A yes works whether the banner answers before or after the script. Cookie Information's, Cookiebot's and Shoporama's events are listened to throughout.
window.affilyflowConsentis checked on load and for the first 10 seconds, and TCF is used when its API is present within those 10 seconds. - A no only deletes on an active action (Cookie Information's and Cookiebot's events, TCF's
useractioncompleteoraffilyflow.consent(false)) – never because the banner hasn't read its own cookie yet. - If you set
window.affilyflowConsentlater than 10 seconds after load, callaffilyflow.consent(true)instead.
Leads without code
For lead programs the script can report a lead by itself when the customer reaches the thank-you page. Put the address in data-lead-url and remove async, so the lead is sent before the thank-you page possibly redirects.
<script src="https://app.affilyflow.com/track.js" data-store="DIN_BUTIK" data-lead-url="/thanks"></script>/thanksmatches/thanksand/thanks/…, but not/thanksgiving.- A full URL (
https://shop.com/thanks) is read as its path. - If parameters are given (
/thanks?type=lead), all of them must be in the page's URL. - The front page alone (
/) only matches when parameters are given too. - The lead is reported once per session and only with an affiliate click in the browser.
Console errors
track.js only writes to the browser console when a sale from the sale line or Google Analytics could not be reported:
| Message | Cause |
|---|---|
Affilyflow: data-affilyflow-order must be the order number – the sale was not recorded. | The sale line's order number is missing or a placeholder. |
Affilyflow: data-affilyflow-total must be the amount (goods excl. VAT and shipping) – the sale was not recorded. | The sale line's amount is empty or 0. |
Affilyflow: the purchase event has no amount – the sale was not recorded. | The Google Analytics purchase had no amount. |
What each response from the server means is in Status codes.
Safari and iPhone
Safari (ITP) deletes cookies and localStorage set from JavaScript after about 7 days without a visit to the site. track.js rewrites its storage on every visit, but cannot and should not get around the limits. Only a cookie set by the store's own server – such as the WooCommerce plugin – lives longer. This applies to every network, not just Affilyflow.
Legacy direct links
Links with ?aff= (or ?aff_id=) and ?store= instead of ?afid= still work. The script counts the click and stores af_aff and af_store. data-store always wins over ?store= in the URL, so a store that uses ?store= itself doesn't get its sales rejected.
Missing something?
Write to us – we answer quickly, and the page gets fixed.