Webhooks
Få hvert salg via en affiliate på jeres egen server, når det sker – og igen, når det godkendes, afvises eller refunderes. Signeret, med genforsøg og log.
En webhook er en adresse på jeres server, som Affilyflow kalder, hver gang der sker noget med et af jeres salg. Brug den til at opdatere jeres ERP eller lager, bogføre provisionen, give besked i Slack eller tjekke salget mod jeres egne ordrer – uden at hente noget selv.
- Hvert kald er signeret med en hemmelighed, kun I og Affilyflow kender.
- Et kald, der ikke får et svar 2xx, prøves igen i op til 24 timer.
- Hvert kald står i en log med svarkode og svar, og I kan sende det igen.
- Send test viser svaret fra jeres server med det samme.
Opret en webhook
- Gå til Indstillinger → Webhooks i butiks-dashboardet.
- Tryk Ny webhook, indsæt adressen, og vælg hændelserne.
- Kopier hemmeligheden. Den vises kun en gang – læg den i en miljøvariabel på serveren, fx
AFFILYFLOW_WEBHOOK_SECRET. - Tryk Send test, og tjek, at jeres server svarer 200.
Ejere og administratorer kan oprette og ændre webhooks; læsere kan se dem og loggen. I kan have op til 5 adresser.
Hændelser
| Hændelse | Sendes når |
|---|---|
sale.created | Et salg via en affiliate er registreret (afventer godkendelse). Også et testkøb fra testlinket – med "test": true. |
sale.approved | Salget er godkendt – af jer eller automatisk efter godkendelsesperioden. |
sale.rejected | Salget er afvist – af jer, ved en hel refusion eller ved brud på programmets regler. |
sale.refunded | Salget er refunderet helt eller delvist. Provision og gebyr står efter refusionen. |
sale.updated | Provision eller gebyr er ændret på anden vis, fx en refusion, der fortrydes, eller et afvist salg, der sættes tilbage til afventer. |
Et salg, der afvises allerede ved registreringen (fx brud på programmets regler), kommer som sale.created med "status": "rejected". En hel refusion giver både sale.refunded (med "type": "full") og sale.rejected. Rækkefølgen er ikke garanteret – brug status og created_at i kroppen.
Kroppen
Hvert kald er en POST med Content-Type: application/json:
{
"id": "evt_6f1c2d0e8b9a4c3d9e7f1a2b3c4d5e6f",
"type": "sale.approved",
"created_at": "2026-11-12T10:14:03.512+00:00",
"test": false,
"data": {
"sale": {
"id": "0b6b2d8e-3f1a-4c55-9d2e-7a1b2c3d4e5f",
"order_id": "100482",
"status": "approved",
"previous_status": "pending",
"currency": "DKK",
"order_value": 999.00,
"commission_base": 759.20,
"commission": 75.92,
"network_fee": 18.98,
"cost": 94.90,
"original_currency": null,
"original_value": null,
"created_at": "2026-10-13T18:22:41.087+00:00",
"approved_at": "2026-11-12T10:14:03.512+00:00",
"approval_due_at": "2026-11-12T18:22:41.087+00:00",
"rejection_reason": null,
"attribution": "click",
"discount_code": null,
"recurring": false,
"billing_cycle": null,
"server_confirmed": true,
"refund": null
},
"affiliate": { "af_number": "AF-03528F", "label": "Hyggelig Hverdag" },
"click": {
"click_id": "c_8Kq2mXo1Tz9w",
"clicked_at": "2026-10-13T17:58:02.114+00:00",
"landing_path": "/produkter/uldplaid",
"affiliate_page": "https://hyggelighverdag.dk/bedste-plaider",
"channel": null,
"device": "mobile",
"country": "DK"
},
"program": { "id": "5d1e2f3a-4b5c-4d6e-8f70-819a2b3c4d5e", "store": "Nordlys Bolig" }
}
}| Felt | Betydning |
|---|---|
id | Hændelsens id. Det samme ved hvert nyt forsøg – gem det, og ignorer en hændelse, I allerede har behandlet. Står også i headeren Affilyflow-Event-Id. |
type | Hændelsen (se tabellen ovenfor). Står også i headeren Affilyflow-Event. |
test | true ved et testkøb fra testlinket og ved Send test. Bogfør det ikke. |
sale.order_id | Jeres ordrenummer. |
sale.status | pending, approved eller rejected – efter hændelsen. previous_status er status før. |
sale.order_value | Ordrens total i currency (jeres afregningsvaluta). commission_base er det beløb, provisionen er regnet af. |
sale.commission | Affiliatens provision. network_fee er Affilyflows gebyr, og cost er de to tilsammen – alle uden moms. Ved rejected viser de, hvad salget havde – I betaler ikke for et afvist salg. |
sale.original_currency | Kundens valuta, når salget er omregnet; original_value er beløbet i den. |
sale.attribution | click (affiliatelink) eller coupon (personlig rabatkode – så står koden i discount_code). |
sale.server_confirmed | true, når jeres server har meldt salget med en API-nøgle. |
sale.refund | Ved en refusion: type (partial eller full), refunded_value, refunded_ratio, commission_before_refund og refunded_at. Ellers null. |
affiliate | Affiliatens visningsnavn eller AF-nummer – det samme, I ser i dashboardet. Aldrig navn, e-mail eller kontaktoplysninger. null ved testkøb. |
click | Klikket bag salget: tidspunkt, landingsside, affiliatens side, enhed og land. null ved salg via en personlig rabatkode. |
Nye felter kan komme til. Ignorer felter, I ikke kender.
Tjek signaturen
Hvert kald har headeren:
Affilyflow-Signature: t=1762942443,v1=5b1f0c…ter tidspunktet i sekunder (Unix-tid).v1er en HMAC-SHA256 med jeres hemmelighed overt, et punktum og den rå krop:"<t>.<krop>", som hex.- Lige efter en ny hemmelighed kan der stå to
v1=– en med den nye og en med den gamle. Godtag kaldet, hvis en af dem passer.
Brug den rå krop, som den kom – ikke JSON, I har læst og skrevet igen. Afvis kald, hvor t er mere end 5 minutter fra jeres ur, så et opsnappet kald ikke kan sendes igen senere.
import crypto from 'node:crypto';
function verifyAffilyflow(rawBody, header, secret, toleranceSec = 300) {
if (!header) return false;
const parts = header.split(',').map((p) => p.trim().split('='));
const t = Number(parts.find(([k]) => k === 't')?.[1]);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
return parts.some(([k, v]) => k === 'v1' && /^[0-9a-f]{64}$/.test(v) && crypto.timingSafeEqual(Buffer.from(v, 'hex'), expected));
}app.post('/affilyflow/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const body = req.body.toString('utf8');
if (!verifyAffilyflow(body, req.get('Affilyflow-Signature'), process.env.AFFILYFLOW_WEBHOOK_SECRET)) {
return res.status(400).send('invalid signature');
}
const event = JSON.parse(body);
// Gem event.id – samme id kommer igen, hvis jeres svar ikke var 2xx.
res.sendStatus(200);
});<?php
function verify_affilyflow(string $rawBody, ?string $header, string $secret, int $tolerance = 300): bool
{
if (!$header) return false;
$t = 0;
$sigs = [];
foreach (explode(',', $header) as $part) {
[$k, $v] = array_pad(explode('=', trim($part), 2), 2, '');
if ($k === 't') $t = (int) $v;
if ($k === 'v1') $sigs[] = $v;
}
if ($t <= 0 || abs(time() - $t) > $tolerance) return false;
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
foreach ($sigs as $sig) {
if (hash_equals($expected, $sig)) return true;
}
return false;
}<?php
$raw = file_get_contents('php://input');
if (!verify_affilyflow($raw, $_SERVER['HTTP_AFFILYFLOW_SIGNATURE'] ?? null, getenv('AFFILYFLOW_WEBHOOK_SECRET'))) {
http_response_code(400);
exit;
}
$event = json_decode($raw, true);
// Gem $event['id'] – samme id kommer igen, hvis jeres svar ikke var 2xx.
http_response_code(200);Svar, genforsøg og pause
- Svar med en status 2xx inden for 10 sekunder. Gør det tunge arbejde bagefter (en kø), hvis det tager længere tid.
- Alt andet – en anden status, en omdirigering, timeout eller en forbindelsesfejl – prøves igen efter cirka 1, 5, 15 og 30 minutter og derefter efter 1, 2, 4, 6 og 8 timer. Efter 24 timer opgives kaldet; det kan stadig sendes igen fra loggen.
- Efter 20 mislykkede forsøg i træk sættes webhooken på pause, og det står i dashboardet. Ret fejlen, send en test, og tryk Slå til. Kald fra de seneste 24 timer bliver så sendt.
- Samme hændelse kan komme mere end en gang (fx hvis jeres svar gik tabt). Brug
idtil at ignorere dubletter.
Test og log
- Send test sender et eksempel på den valgte hændelse med
"test": trueog ordrenummeretTEST-1001og viser svaret med det samme. En test prøves kun en gang og tæller ikke med i pausen. - Log viser de seneste 20 kald pr. adresse: hændelse, ordre, status, svarkode, antal forsøg, næste forsøg og de første 500 tegn af jeres svar. Send igen sender et kald igen. Loggen gemmes i 30 dage.
Ny hemmelighed
Tryk Ny hemmelighed for at skifte den. Som standard signeres kaldene med både den nye og den gamle i 24 timer, så I kan opdatere serveren uden at afvise noget. Er den gamle sluppet ud, så slå det fra – så gælder kun den nye fra næste kald.
Hvad med salg fra jeres egen server?
Webhooks går fra Affilyflow til jer. Vil I melde salg og refusioner til os fra jeres server, så brug butikkens API med nøgle – se også Server-sporing.
Mangler du noget?
Skriv til os – vi svarer hurtigt, og siden bliver rettet.