Sporing fra jeres server

Lad jeres egen server gemme affiliate-klikket og melde salget – så holder sporingen på iPhone, i Safari og med annonceblokkere.

Se som Markdown

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:

CookieIndhold
af_clickKlik-id'et fra ?afid=
af_tKlikkets tidspunkt i sekunder
af_storeJeres butiks-id
af_srv1 – 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

Laravel · middlewarePHP
<?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 · kør på hver side før outputPHP
<?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)

Node.js (Express)JavaScript
// 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.tsTypeScript
// 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:

  1. Ved checkout – mens det er kundens egen browser, der kalder jer – gemmer I værdien af cookien af_click på ordren, fx i en kolonne affilyflow_click.
  2. Når ordren er betalt – fx i betalingens callback eller webhook – melder I ordren til konverterings-API'et.

Laravel

Laravel · når ordren er betaltPHP
<?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 · når ordren er betaltPHP
<?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)

Node.js · når ordren er betaltJavaScript
// 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

Next.js · når ordren er betaltTypeScript
// 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:

FeltPåkrævetBetydning
sourceAnbefalet"server" – markerer, at salget kommer fra jeres server.
storeJaJeres butiks-id.
afidNår der er et klikKlik-id'et, I gemte på ordren ved checkout.
orderJaOrdrenummeret, som kunden ser det. Samme nummer tælles kun en gang.
totalJaVarerne efter rabat, uden moms og fragt, som et maskintal (540.00).
currencyNejValutakode, fx DKK eller EUR. Standard er DKK.
couponsNejKundens rabatkoder som en liste, fx ["ANNA10"]. Er en af dem en personlig rabatkode, giver den provision – også uden klik.
buyer_hashAnbefaletsha256 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

nginxTekst
# 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

ApacheTekst
# 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.tsTypeScript
// 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 aldrig af_click eller af_t igen – 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

  1. Tryk Åbn testlink under testtrinnet i Opsætning, og læg en testordre i samme browser.
  2. Live-testen viser testklikket og købet med ordrenummer og beløb. Tjek, at beløbet er varerne efter rabat, uden moms og fragt.
  3. Tjek i browserens udviklerværktøjer, at cookierne af_click, af_t, af_store og af_srv er 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.

Kontakt support