Sporing fra jeres server
Lad jeres egen server gemme affiliate-klikket og melde salget – så holder sporingen på iPhone, i Safari og med annonceblokkere.
Til en specialbygget webshop kan jeres egen server overtage de to vigtigste opgaver fra scriptet: at gemme affiliate-klikket og at melde salget. Det er valgfrit, men det er den mest robuste opsætning. Kodestumperne står også under Program → Opsætning med jeres butiks-id sat ind; her står DIN_BUTIK i stedet.
Hvorfor
- Safari og iPhone. Safari sletter cookies og localStorage, som er sat af JavaScript, efter cirka 7 dage uden besøg på siden. En kunde, der klikker i dag og køber om to uger, er så væk – også selvom programmets cookie-periode er 30 dage. En cookie, som jeres egen server sætter, forkortes ikke på den måde.
- Annonceblokkere. En blokker kan stoppe et sporingsscript og dets kald. Den kan ikke stoppe jeres egen server i at sætte en cookie eller melde en betalt ordre.
- Kvitteringen. Når serveren melder salget, betyder det ikke noget, om kunden ser kvitteringssiden – fx hvis betalingen bliver bekræftet senere, eller kunden lukker fanen.
Scriptet og salgslinjen kan blive, hvor de er. Se Sådan spiller det sammen med scriptet.
Klikket fra jeres server
En lille middleware gemmer ?afid= fra affiliate-linket som førstepartscookies på jeres domæne:
| Cookie | Indhold |
|---|---|
af_click | Klik-id'et fra ?afid= |
af_t | Klikkets tidspunkt i sekunder |
af_store | Jeres butiks-id |
af_srv | 1 – fortæller scriptet, at jeres server har sat cookien |
- Cookierne lever 90 dage fra klikket. Ved et genbesøg fornyes de med den resterende tid – aldrig ud over 90 dage fra klikket, fordi tidspunktet står i
af_t. Programmets egen cookie-periode håndhæves stadig på vores server. - Kun et
afid, der ligner vores (c_efterfulgt af 4–64 tegn: bogstaver, tal,_og-), gemmes. - Cookierne må ikke være HttpOnly: track.js læser dem på kvitteringen.
- Kør koden på hver side, før der sendes output, så klikket gemmes, uanset hvilken side kunden lander på.
Laravel
<?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]);
// })Laravel krypterer cookies som standard. Undtag de fire cookies som vist nederst i kodestumpen – ellers kan track.js ikke læse dem.
PHP
<?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);
}
}Node.js (Express)
// Affilyflow: gem affiliate-klikket fra jeres egen server, så det holder i hele perioden –
// også på iPhone/Safari og med annonceblokkere. Kræver cookie-parser (app.use(cookieParser())).
// Cookierne må IKKE være httpOnly (track.js læser dem).
const AF_ID = /^c_[A-Za-z0-9_-]{4,64}$/;
const AF_MAX = 90 * 86400;
app.use((req, res, next) => {
const set = (name, value, age) =>
res.cookie(name, value, { maxAge: age * 1000, path: '/', sameSite: 'lax', secure: req.secure, httpOnly: false });
const afid = String(req.query.afid || '');
if (AF_ID.test(afid)) {
// Nyt klik fra et affiliate-link.
for (const [k, v] of [['af_click', afid], ['af_t', String(Math.floor(Date.now() / 1000))], ['af_store', 'DIN_BUTIK'], ['af_srv', '1']]) set(k, v, AF_MAX);
return next();
}
// Genbesøg: forny cookien – men aldrig ud over 90 dage fra klikket.
const click = String(req.cookies?.af_click || '');
const t = Number(req.cookies?.af_t || 0);
const left = AF_MAX - (Math.floor(Date.now() / 1000) - t);
if (t > 0 && left > 0 && AF_ID.test(click)) {
for (const [k, v] of [['af_click', click], ['af_t', String(t)], ['af_store', 'DIN_BUTIK'], ['af_srv', '1']]) set(k, v, left);
}
next();
});Next.js
// middleware.ts – Affilyflow: gem affiliate-klikket fra jeres egen server, så det holder i hele
// perioden – også på iPhone/Safari og med annonceblokkere. Cookierne må IKKE være httpOnly.
import { NextResponse, type NextRequest } from 'next/server';
const AF_ID = /^c_[A-Za-z0-9_-]{4,64}$/;
const AF_MAX = 90 * 86400;
export function middleware(req: NextRequest) {
const res = NextResponse.next();
const set = (name: string, value: string, age: number) =>
res.cookies.set(name, value, { maxAge: age, path: '/', sameSite: 'lax', secure: true, httpOnly: false });
const afid = req.nextUrl.searchParams.get('afid') ?? '';
if (AF_ID.test(afid)) {
// Nyt klik fra et affiliate-link.
set('af_click', afid, AF_MAX);
set('af_t', String(Math.floor(Date.now() / 1000)), AF_MAX);
set('af_store', 'DIN_BUTIK', AF_MAX);
set('af_srv', '1', AF_MAX);
return res;
}
// Genbesøg: forny cookien – men aldrig ud over 90 dage fra klikket.
const click = req.cookies.get('af_click')?.value ?? '';
const t = Number(req.cookies.get('af_t')?.value ?? 0);
const left = AF_MAX - (Math.floor(Date.now() / 1000) - t);
if (t > 0 && left > 0 && AF_ID.test(click)) {
set('af_click', click, left);
set('af_t', String(t), left);
set('af_store', 'DIN_BUTIK', left);
set('af_srv', '1', left);
}
return res;
}
export const config = { matcher: ['/((?!_next/|api/|.*\\.[a-z0-9]+$).*)'] };Vent på samtykke
Står Cookiebanner på Vent på samtykke i Opsætning, får kodestumperne i Opsætning en ekstra funktion, så cookierne først sættes, når Cookiebot eller Cookie Information viser samtykke til marketing. Bruger I et andet banner, skal den funktion rettes til jeres. Se Samtykke og cookies.
Salget fra jeres server
Salget meldes i to trin:
- Ved checkout – mens det er kundens egen browser, der kalder jer – gemmer I værdien af cookien
af_clickpå ordren, fx i en kolonneaffilyflow_click. - Når ordren er betalt – fx i betalingens callback eller webhook – melder I ordren til konverterings-API'et.
Laravel
<?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);
}PHP
<?php
// 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 !== []);
$ch = curl_init('https://app.affilyflow.com/api/track/conversion');
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($body),
]);
curl_exec($ch);
curl_close($ch);
}Node.js (Express)
// Affilyflow: meld salget fra jeres server, når ordren er BETALT.
// 1) Ved checkout (kundens egen browser): gem klikket på ordren, fx order.affilyflowClick = req.cookies.af_click
// 2) Når betalingen er bekræftet (fx i betalingens webhook), kald:
import { createHash } from 'node:crypto';
export async function affilyflowSale({ orderId, goodsExVat, currency, click, coupons = [], email }) {
// Uden klik og uden en rabatkode er der intet at melde.
if (!click && !coupons.length) return;
await fetch('https://app.affilyflow.com/api/track/conversion', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
source: 'server',
store: 'DIN_BUTIK',
afid: click || undefined, // klik-id'et fra ordren
order: orderId, // samme ordrenummer som kunden ser
total: Math.round(goodsExVat * 100) / 100, // varerne efter rabat, UDEN moms og fragt
currency, // fx DKK
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 ? createHash('sha256').update(email.trim().toLowerCase()).digest('hex') : undefined,
}),
signal: AbortSignal.timeout(5000),
}).catch(() => {}); // sporingen må aldrig vælte en ordre
}Next.js
// Affilyflow: meld salget fra jeres server, når ordren er BETALT.
// 1) Ved checkout (kundens egen browser): gem klikket på ordren, fx order.affilyflowClick = req.cookies.af_click
// 2) Når betalingen er bekræftet (fx i betalingens webhook), kald:
import { createHash } from 'node:crypto';
export async function affilyflowSale({ orderId, goodsExVat, currency, click, coupons = [], email }: {
orderId: string; goodsExVat: number; currency: string; click?: string | null; coupons?: string[]; email?: string | null;
}) {
// Uden klik og uden en rabatkode er der intet at melde.
if (!click && !coupons.length) return;
await fetch('https://app.affilyflow.com/api/track/conversion', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
source: 'server',
store: 'DIN_BUTIK',
afid: click || undefined, // klik-id'et fra ordren
order: orderId, // samme ordrenummer som kunden ser
total: Math.round(goodsExVat * 100) / 100, // varerne efter rabat, UDEN moms og fragt
currency, // fx DKK
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 ? createHash('sha256').update(email.trim().toLowerCase()).digest('hex') : undefined,
}),
signal: AbortSignal.timeout(5000),
}).catch(() => {}); // sporingen må aldrig vælte en ordre
}Felterne
Kaldet er en POST med JSON til https://app.affilyflow.com/api/track/conversion:
| Felt | Påkrævet | Betydning |
|---|---|---|
source | Anbefalet | "server" – markerer, at salget kommer fra jeres server. |
store | Ja | Jeres butiks-id. |
afid | Når der er et klik | Klik-id'et, I gemte på ordren ved checkout. |
order | Ja | Ordrenummeret, som kunden ser det. Samme nummer tælles kun en gang. |
total | Ja | Varerne efter rabat, uden moms og fragt, som et maskintal (540.00). |
currency | Nej | Valutakode, fx DKK eller EUR. Standard er DKK. |
coupons | Nej | Kundens rabatkoder som en liste, fx ["ANNA10"]. Er en af dem en personlig rabatkode, giver den provision – også uden klik. |
buyer_hash | Anbefalet | sha256 af køberens e-mail med små bogstaver og uden mellemrum foran og bagved. Bruges kun til at afvise selvhenvisning. E-mailen selv sendes aldrig. |
- Uden klik og uden rabatkoder er der intet at melde – kodestumperne springer kaldet over.
- Kaldet har en timeout på 5 sekunder. Lad det aldrig afgøre, om ordren gennemføres – kald det, når ordren er gemt som betalt.
- Svarene og deres betydning står i Konverterings-API og Statuskoder.
Egen proxy (valgfri)
Vil I også have scriptet og dets kald væk fra vores domæne, kan jeres webserver sende alt under /af/ videre til Affilyflow. Scriptet hentes så fra /af/s.js på jeres eget domæne, og fordi scriptet sender sine kald ved siden af sig selv, går de også gennem jeres domæne. En annonceblokker ser kun jeres egne adresser.
s.js er den samme fil som track.js – navnet er blot neutralt. Bagefter skifter I scriptet i <head> ud med linjen nederst i kodestumpen.
nginx
# 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/s.js" data-store="DIN_BUTIK" async></script>Apache
# Apache (mod_proxy + mod_ssl): alt under /af/ sendes videre til Affilyflow
SSLProxyEngine On
ProxyPass /af/ https://app.affilyflow.com/
ProxyPassReverse /af/ https://app.affilyflow.com/
<!-- Scriptet hentes nu fra jeres eget domæne (alle sider, i <head>) -->
<script src="/af/s.js" data-store="DIN_BUTIK" async></script>Next.js
// next.config.ts: alt under /af/ sendes videre til Affilyflow
async rewrites() {
return [{ source: '/af/:path*', destination: 'https://app.affilyflow.com/:path*' }];
}
<!-- Scriptet hentes nu fra jeres eget domæne (alle sider, i <head>) -->
<script src="/af/s.js" data-store="DIN_BUTIK" async></script>Bruger scriptet data-consent, data-sale eller andre attributter i dag, så behold dem på den nye linje.
Sådan spiller det sammen med scriptet
- Scriptet kan blive. Har jeres server sat cookien, står
af_srv=1, og så skriver track.js aldrigaf_clickelleraf_tigen – en cookie, som JavaScript skriver, ville Safari forkorte til cirka 7 dage, også selvom jeres server satte den først. Scriptet gemmer stadig klikket i localStorage som reserve. - Samme ordre tæller kun en gang. Melder både jeres server og browseren (scriptet eller salgslinjen) samme ordre med samme ordrenummer, registreres salget en gang. Browserens melding kan derfor blive stående som reserve.
- Klikket følger med. Scriptet læser
af_click, uanset om jeres server eller scriptet satte den, så salgslinjen på kvitteringen virker som før. - Et nej sletter alt. Siger en kunde aktivt nej til marketing, sletter scriptet også de cookies, jeres server har sat. Sæt dem ikke igen uden samtykke.
Test
- Tryk Åbn testlink under testtrinnet i Opsætning, og læg en testordre i samme browser.
- Live-testen viser testklikket og købet med ordrenummer og beløb. Tjek, at beløbet er varerne efter rabat, uden moms og fragt.
- Tjek i browserens udviklerværktøjer, at cookierne
af_click,af_t,af_storeogaf_srver sat på jeres domæne efter testlinket.
Bagefter viser Sporingens dækning i Opsætning, hvor mange klik fra affiliates der når frem, bliver gemt og bliver til et salg. Mere i Test og fejlfinding.
Mangler du noget?
Skriv til os – vi svarer hurtigt, og siden bliver rettet.