Server-side tracking
Let your own server store the affiliate click and report the sale – so tracking holds up on iPhone, in Safari and with ad blockers.
On a custom-built store, your own server can take over the two most important jobs from the script: storing the affiliate click and reporting the sale. It's optional, but it's the most robust setup you can have. The snippets are also in Program → Setup under For developers: even more precise tracking, with your store ID filled in; here, DIN_BUTIK stands in for it.
Why
- Safari and iPhone. Safari deletes cookies and localStorage set by JavaScript after about 7 days without a visit to the site. A customer who clicks today and buys in two weeks is lost – even if the program's cookie period is 30 days. A cookie set by your own server isn't shortened in that way.
- Ad blockers. A blocker can stop a tracking script and its requests. It can't stop your own server from setting a cookie or reporting a paid order.
- The order confirmation. When the server reports the sale, it doesn't matter whether the customer ever sees the confirmation page – for example if the payment is confirmed later, or the customer closes the tab.
The script and the sale line can stay where they are. See How it works with the script.
The click from your server
A small middleware stores ?afid= from the affiliate link as first-party cookies on your domain:
| Cookie | Contents |
|---|---|
af_click | The click ID from ?afid= |
af_t | When the click happened, in seconds |
af_store | Your store ID |
af_srv | 1 – tells the script that your server set the cookie |
- The cookies last 90 days from the click. On a return visit they are renewed with the time that's left – never beyond 90 days from the click, because the time of the click is in
af_t. The program's own cookie period is still enforced on our server. - Only an
afidthat looks like ours (c_followed by 4–64 characters: letters, digits,_and-) is stored. - The cookies must not be HttpOnly: track.js reads them on the order confirmation.
- Run the code on every page before any output is sent, so the click is stored whichever page the customer lands on.
- Laravel encrypts cookies by default. Exclude the four cookies as shown at the bottom of the Laravel snippet – otherwise track.js can't read them.
<?php
// Affilyflow: gem affiliate-klikket fra jeres egen server, så det holder i hele perioden –
// også på iPhone/Safari og med annonceblokkere. Kald affilyflow_click() på hver side,
// før der sendes output. Cookierne må IKKE være HttpOnly (track.js læser dem).
function affilyflow_click(): void {
$max = 90 * 86400;
$set = function (string $name, string $value, int $age): void {
setcookie($name, $value, [
'expires' => time() + $age, 'path' => '/', 'samesite' => 'Lax',
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
]);
};
$afid = (string) ($_GET['afid'] ?? '');
if (preg_match('/^c_[A-Za-z0-9_-]{4,64}$/', $afid)) {
// Nyt klik fra et affiliate-link.
foreach (['af_click' => $afid, 'af_t' => (string) time(), 'af_store' => 'DIN_BUTIK', 'af_srv' => '1'] as $k => $v) $set($k, $v, $max);
return;
}
// Genbesøg: forny cookien – men aldrig ud over 90 dage fra klikket.
$click = (string) ($_COOKIE['af_click'] ?? '');
$t = (int) ($_COOKIE['af_t'] ?? 0);
$left = $max - (time() - $t);
if ($t > 0 && $left > 0 && preg_match('/^c_[A-Za-z0-9_-]{4,64}$/', $click)) {
foreach (['af_click' => $click, 'af_t' => (string) $t, 'af_store' => 'DIN_BUTIK', 'af_srv' => '1'] as $k => $v) $set($k, $v, $left);
}
}
// Laravel: kald den i en middleware (app/Http/Middleware/AffilyflowClick.php) …
// public function handle($request, \Closure $next) { affilyflow_click(); return $next($request); }
// … og undtag cookierne fra Laravels kryptering i bootstrap/app.php:
// ->withMiddleware(function ($middleware) {
// $middleware->encryptCookies(except: ['af_click', 'af_t', 'af_store', 'af_srv']);
// $middleware->web(append: [\App\Http\Middleware\AffilyflowClick::class]);
// })Waiting for consent
If Cookie banner is set to Wait for consent in Setup, the snippets in Setup get an extra function, so the cookies are only set once Cookiebot or Cookie Information reports consent to marketing. If you use a different banner, adapt that function to yours. See Consent and cookies.
The sale from your server
The sale is reported in two steps:
- At checkout – while it's the customer's own browser calling you – store the value of the
af_clickcookie on the order, for example in anaffilyflow_clickcolumn. - When the order is paid – for example in the payment callback or webhook – report the order to the conversion API.
<?php
use Illuminate\Support\Facades\Http;
// Affilyflow: meld salget fra jeres server, når ordren er BETALT.
// 1) Ved checkout (kundens egen browser): gem klikket på ordren, fx i en kolonne affilyflow_click:
// $order->affilyflow_click = $_COOKIE['af_click'] ?? null;
// 2) Når betalingen er bekræftet (fx i betalings-callbacken), kald:
function affilyflow_sale(string $orderId, float $goodsExVat, string $currency, ?string $click, array $coupons = [], ?string $email = null): void {
// Uden klik og uden en rabatkode er der intet at melde.
if (!$click && !$coupons) return;
$body = array_filter([
'source' => 'server',
'store' => 'DIN_BUTIK',
'afid' => $click, // klik-id'et fra ordren
'order' => $orderId, // samme ordrenummer som kunden ser
'total' => round($goodsExVat, 2), // varerne efter rabat, UDEN moms og fragt
'currency' => $currency, // fx DKK
'coupons' => array_values($coupons), // kundens rabatkoder (giver provision, hvis en er personlig)
// Fingeraftryk af e-mailen, så selvhenvisning kan afvises. E-mailen selv sendes aldrig.
'buyer_hash' => $email ? hash('sha256', strtolower(trim($email))) : null,
], fn ($v) => $v !== null && $v !== []);
Http::timeout(5)->post('https://app.affilyflow.com/api/track/conversion', $body);
}The fields
The call is a POST with JSON to https://app.affilyflow.com/api/track/conversion:
| Field | Required | Meaning |
|---|---|---|
source | Recommended | "server" – marks the sale as coming from your server. |
store | Yes | Your store ID. |
afid | When there is a click | The click ID you stored on the order at checkout. |
order | Yes | The order number as the customer sees it. The same number is only counted once. |
total | Yes | The goods after discount, excluding VAT and shipping, as a machine number (540.00). |
currency | No | Currency code, e.g. DKK or EUR. Without it, the store's settlement currency is used. |
coupons | No | The customer's discount codes as a list, e.g. ["ANNA10"]. If one of them is a personal discount code, it earns commission – even without a click. |
buyer_hash | Recommended | sha256 of the buyer's email in lower case, with no leading or trailing spaces. Used only to reject self-referrals. The email itself is never sent. |
- With no click and no discount codes there is nothing to report – the snippets skip the call.
- The call has a 5-second timeout. Never let it decide whether the order goes through – call it once the order is saved as paid.
- The responses and what they mean are in Conversion API and Status codes.
Your own proxy (optional)
If you also want the script and its requests off our domain, your web server can forward everything under /af/ to Affilyflow. The script is then loaded from /af/t/DIN_BUTIK.js on your own domain, and because the script sends its requests to the folder it was loaded from, they go through your domain too. An ad blocker only sees your own addresses.
It's the same script as before, with your settings from Setup. Afterwards, replace the script in <head> with the line at the bottom of the snippet.
# nginx: alt under /af/ sendes videre til Affilyflow
location /af/ {
proxy_pass https://app.affilyflow.com/;
proxy_set_header Host app.affilyflow.com;
proxy_ssl_server_name on;
}
<!-- Scriptet hentes nu fra jeres eget domæne (alle sider, i <head>) -->
<script src="/af/t/DIN_BUTIK.js" async></script>Your settings from Setup (Google Analytics, VAT, shipping, consent) come along, because our server adds them to the script. The older proxy line <script src="/af/s.js" data-store="DIN_BUTIK" async></script> still works – if you use it, keep its attributes such as data-consent and data-sale.
How it works with the script
- The script can stay. Once your server has set the cookie,
af_srv=1is present, and track.js never writesaf_clickoraf_tagain – Safari would shorten a cookie written by JavaScript to about 7 days, even if your server set it first. The script still keeps the click in localStorage as a fallback. - The same order only counts once. If both your server and the browser (the script or the sale line) report the same order with the same order number, the sale is recorded once. The browser report can therefore stay in place as a fallback.
- The click comes along. The script reads
af_clickwhether your server or the script set it, so the sale line on the order confirmation works as before. - A no removes everything. If a customer actively declines marketing, the script also deletes the cookies your server set. Don't set them again without consent.
Test tracking
- Go to the Test tracking step in Setup, press Open test link, and place a test order in the same browser.
- The live test shows the test click and the purchase with order number and amount. Check that the amount is the goods after discount, excluding VAT and shipping.
- In the browser's developer tools, check that the
af_click,af_t,af_storeandaf_srvcookies are set on your domain after the test link.
Afterwards, Tracking coverage in Setup shows how many affiliate clicks arrive, are stored and turn into a sale. More in Testing and troubleshooting.
Missing something?
Write to us – we answer quickly, and the page gets fixed.