CheckoutGids
Waarom kortingscodes lekken, en hoe je ze native vervangt
Diek Thunnissen8 sep 202614 min leestijd
Een kortingscode lekt drie dingen: marge, meetbaarheid en zichtbaarheid. Shopify heeft voor bijna elk gebruik van een code inmiddels een native alternatief. Creator-deals via de link, VIP-korting op een segment, een kortingsbodem per collectie, en een code die zichzelf afknipt in plaats van te weigeren. Met de Functions erbij.

Bijna elke Shopify-shop deelt kortingscodes uit, en bijna elke eigenaar weet stiekem dat ze weglekken. De code van je grootste creator staat binnen een week op de couponsites. De VIP-code uit de nieuwsbrief wordt doorgestuurd. En het lege veld "kortingscode?" in de checkout stuurt kopers naar Google, waar ze een code vinden die nooit voor hen bedoeld was.
Het rare is dat we het allemaal zo blijven doen. Niet omdat het goed werkt, maar omdat er lang geen alternatief was. Dat is veranderd. Voor bijna elk gebruik van een kortingscode heeft Shopify inmiddels iets natives dat niet kan lekken, en waar dat niet zo is, kun je de code zelf een brein geven. Hieronder de drie vervangers die we bouwen, met de code erbij.
Wat een code precies lekt
Ten eerste marge. Een code is een string, en een string reist. Op couponsites, in appgroepen, in de comments onder de post van de creator. Elke order met een gelekte code is korting aan iemand die zonder die code ook had gekocht.
Ten tweede meetbaarheid. Codes zijn ook je attributiesysteem voor influencers en affiliates. Zodra de code rondzwerft, weet je niet meer of een order van haar publiek kwam of van iemand die even korting googelde. Je betaalt commissie op ruis en je stuurt je creator-budget op een cijfer dat niet klopt.
Ten derde je zichtbaarheid. Een code bestaat pas in de checkout. Op je collectiepagina, in je Google Shopping-feed en in de vergelijking met je concurrent staat gewoon de volle prijs. Daar heeft die code niemand over de streep getrokken, want daar was hij onzichtbaar. Een automatische korting of een echte sale-prijs met compare-at staat wél in de feed.
En dan is er nog het stapelen. Elke korting is los bedacht: de staffel, de welkomstdeal, de creator-code, de campagne van marketing. Los zijn ze allemaal prima. Samen zijn ze een gat in je marge dat niemand heeft besloten.
Vervanger 1: de creator-deal zonder code
De influencer-korting is het schoolvoorbeeld. Je wilt twee dingen: dat haar publiek een deal krijgt, en dat jij weet welke orders van haar kwamen. Een code doet allebei slecht. De vervanger doet allebei goed: de link ís de code.
Op de demo-store heeft elke creator een eigen landingspagina, en die pagina zet een cart attribute "bron" met de naam van de creator. Kom je via een gewone link met een utm_source binnen, dan gebeurt hetzelfde. Een Shopify Function in de checkout leest dat attribute, zoekt de deal op in één json-veld op de shop, en zet de korting erop. Geen code, niks te googlen, niks door te sturen.
Stap één is de snippet in het thema die de utm_source omzet naar een cart attribute. Hij gebruikt de standaard storefront actions, dus hij werkt in elk thema dat die ondersteunt, en hij onthoudt de bron in de sessie voor als iemand pas later iets in zijn winkelwagen legt.
42 regels codeToon code +Verberg code −
{%- comment -%}
Kanaal-checkout stap 1: zet utm_source om naar cart attribute `bron` via de
standaard updateCart-action (standard storefront actions, aug 2026).
Gebruik: {% render 'channel-attribute' %} onderaan theme.liquid.
Let op: het attributes-argument van updateCart VERVANGT de volledige set,
dus we lezen eerst de huidige attributes via getCart en mergen. De waarde
blijft in sessionStorage staan zodat hij ook gezet wordt als de bezoeker
pas later iets in de cart legt (updateCart op een lege sessie is zinloos).
{%- endcomment -%}
<script>
(function () {
var KEY = 'diek_bron';
var params = new URLSearchParams(window.location.search);
var src = params.get('utm_source');
if (src) {
src = src.toLowerCase().replace(/[^a-z0-9_-]/g, '').slice(0, 32);
if (src) sessionStorage.setItem(KEY, src);
}
var bron = sessionStorage.getItem(KEY);
if (!bron) return;
function apply() {
if (!(window.Shopify && Shopify.actions && Shopify.actions.updateCart && Shopify.actions.getCart)) return;
Shopify.actions.getCart().then(function (result) {
var cart = result && result.cart;
var existing = (cart && cart.attributes) || [];
var found = existing.some(function (a) { return a.key === 'bron' && a.value === bron; });
if (found) return;
var merged = existing.filter(function (a) { return a.key !== 'bron'; });
merged.push({ key: 'bron', value: bron });
return Shopify.actions.updateCart({ attributes: merged });
}).catch(function () { /* stil falen: attribute is nice-to-have */ });
}
if (document.readyState === 'complete') apply();
else window.addEventListener('load', apply);
// Ook na cart-wijzigingen opnieuw borgen (bv. cart gestart ná landing):
document.addEventListener('shopify:cart:lines-update', apply);
})();
</script>Stap twee is de configuratie. Eén json-veld op de shop, per creator een percentage en een label. Nieuwe creator erbij is één regel in de admin, geen deploy.
// Shop-metafield custom.creator_deals (json)
{
"diek": { "percent": 10, "label": "Diek's deal: 10% via zijn link" },
"lisa": { "percent": 15, "label": "Lisa's deal" }
}Stap drie is de Function. De input query haalt het attribute, de cart-regels en het shop-veld op. Pre-orders zijn uitgesloten, zoals bij al onze productkortingen.
# extensions/creator-deal/src/cart_lines_discounts_generate_run.graphql
query CartInput {
cart {
bron: attribute(key: "bron") {
value
}
lines {
id
quantity
merchandise {
__typename
... on ProductVariant {
product {
releaseDate: metafield(namespace: "custom", key: "preorder_release_date") {
value
}
}
}
}
}
}
shop {
localTime {
date
}
deals: metafield(namespace: "custom", key: "creator_deals") {
jsonValue
}
}
discount {
discountClasses
}
}En de kern van de logica: geen bron, geen deal. Wel een bron die in de config staat, dan de korting op alle regels die geen pre-order zijn. Het label komt uit hetzelfde veld, dus de klant ziet "Diek's deal: 10% via zijn link" in plaats van een codenaam.
53 regels codeToon code +Verberg code −
// extensions/creator-deal/src/cart_lines_discounts_generate_run.ts (kern)
export function cartLinesDiscountsGenerateRun(
input: CartInput,
): CartLinesDiscountsGenerateRunResult {
if (!input.cart.lines.length) {
return { operations: [] };
}
if (!input.discount.discountClasses.includes(DiscountClass.Product)) {
return { operations: [] };
}
const bron = String(input.cart.bron?.value ?? '').toLowerCase();
if (!bron) {
return { operations: [] };
}
const deal = parseDeals(input.shop.deals?.jsonValue)[bron];
if (!deal) {
return { operations: [] };
}
const today = input.shop.localTime.date;
const eligibleLines = input.cart.lines.filter((line) => {
if (line.merchandise.__typename !== 'ProductVariant') return false;
const release = line.merchandise.product.releaseDate?.value;
return !release || release <= today;
});
if (eligibleLines.length === 0) {
return { operations: [] };
}
return {
operations: [
{
productDiscountsAdd: {
candidates: [
{
message: deal.label ?? `Creator-deal ${deal.percent}%`,
targets: eligibleLines.map((line) => ({
cartLine: { id: line.id },
})),
value: {
percentage: { value: deal.percent },
},
},
],
selectionStrategy: ProductDiscountSelectionStrategy.First,
},
},
],
};
}De attributie zit nu in de order zelf, als attribute. Geen code die kan zwerven, geen commissie op iemand die de code van een couponsite plukte. Wie serieus budget op creators draait, kan nog een stap verder: sinds Storefront API 2026-10 kun je een heel verkoopkanaal een eigen catalogus geven, met eigen prijzen en eigen assortiment, tot in de checkout. Orders hangen dan aan dat kanaal, en de attributie per kanaal is sinds juni 2026 native. Dat is custom werk, een eigen sales channel en catalogs via de API, dus voor een shop die twee keer per jaar iets met een influencer doet is het onzin. Voor wie er maandelijks tienduizenden euro's in stopt, is het een van de weinige manieren om die meting echt kloppend te krijgen.
query ($channelId: ID!) @inContext(channelId: $channelId) {
product(handle: "ceramico-verde") {
availableForSale
priceRange { minVariantPrice { amount } }
}
}Vervanger 2: VIP-korting op een klantsegment
Het klassieke recept: je wilt je vaste klanten belonen, dus er gaat een code in een mailtje. Vanaf dat moment ben je de controle kwijt. De code wordt doorgestuurd, opgepikt door couponsites, en maanden later geeft hij nog steeds korting aan mensen voor wie hij nooit bedoeld was.
Sinds augustus 2025 kun je bij een automatische korting kiezen wie hem krijgt: alle klanten, geselecteerde segmenten (maximaal vijf per korting) of individuele klanten. Een segment is gewoon een filter op je klantdata. Minimaal vijf orders. Meer dan duizend euro besteed. Een VIP-tag. Zelfs een eigen metafield. Zit een klant in het segment, dan krijgt hij de korting vanzelf in de checkout. Zit hij er niet in, dan bestaat de korting voor hem niet. Er valt niks door te sturen.
Hier is geen code voor nodig, letterlijk niet. Dit is admin-werk.
Segment "VIP" (Klanten > Segmenten), voorbeeldfilter: number_of_orders >= 5 AND amount_spent >= 1000 Automatische korting "VIP-voordeel 10%": Type: bedrag korting op bestelling Klanten die in aanmerking komen: geselecteerde segmenten → VIP Combinaties: productkortingen aan, orderkortingen uit Geen code. Geen einddatum nodig: wie uit het segment valt, verliest de korting vanzelf.
VIP-voordeel, loyalty-tiers, winback voor slapende klanten, personeelskorting. Allemaal zonder code. Eén kanttekening: de klant moet ingelogd zijn, anders weet de checkout niet wie er staat. We zien dat niet als nadeel. Je geeft mensen een echte reden om een account te hebben, en met de nieuwe klantaccounts (inlogcode per mail, geen wachtwoord) is die drempel laag.
Wil je meer dan een segment kan, bijvoorbeeld een korting die realtime uit het bestede bedrag een tier berekent en dat bedrag ook in de checkout toont, dan wordt het een discount Function. Die ziet orderaantal, besteding, tags, metafields en sinds API 2026-10 ook de leeftijd van het account. Segmenten zijn de no-code route, Functions de maatwerkroute. Ons memberprogramma op de demo-store is die maatwerkroute, en ook daar bestaat geen enkele code.
Vervanger 3: de kortingsbodem per collectie
Dit is de vervanger voor het stapelprobleem, en hij gaat niet over codes vermijden maar over de schade begrenzen. Elke korting in je shop kent alleen zichzelf. De staffel weet niet dat er een welkomstdeal loopt, de welkomstdeal weet niet dat marketing net een code heeft aangezet. Combinaties uitzetten is de standaardoplossing, maar dat is alles of niets: dan mag een welkomstcode nooit meer bovenop een staffel, ook als de marge het prima toelaat.
De bodem zegt niet welke kortingen mogen stapelen, maar hoeveel. Eén json-veld op de shop: een maximum voor de hele shop en een strenger maximum per collectie waar de marge dunner is.
// Shop-metafield custom.discount_guardrails (json), stand demo-store 3 sep 2026
{
"default_max_pct": 40,
"collections": [
{ "id": "gid://shopify/Collection/452938072298", "title": "Truien & Knitwear", "max_pct": 35 },
{ "id": "gid://shopify/Collection/452938137834", "title": "Broeken", "max_pct": 35 }
],
"codes": {
"WELKOM10": { "percent": 10, "label": "Welkomstcode" }
}
}De bewaker is een Cart and Checkout Validation Function. Bewust een validation en geen discount: een discount Function kan alleen zijn eigen korting berekenen en die van een ander niet afknippen, want Functions draaien naast elkaar en zien elkaar niet. De bodem moet over álle kortingen heen gelden, dus hij zit op de kassa. Per cart-regel telt hij alle korting die erop ligt, automatisch, via code, van welke app dan ook, en de order-korting die over de regels verdeeld is, en vergelijkt dat met de strengste bodem van de collecties waar het product in zit.
// extensions/discount-guardrail/src/cart_validations_generate_run.ts (kern)
for (const line of input.cart.lines) {
if (line.merchandise.__typename !== "ProductVariant") continue;
const base = Number(line.cost.subtotalAmount.amount);
if (!(base > 0)) continue;
const allocated = line.discountAllocations.reduce(
(sum, a) => sum + Number(a.discountedAmount.amount),
0,
);
const viaTotal = base - Number(line.cost.totalAmount.amount);
const discount = Math.max(allocated, viaTotal, 0);
const pct = (discount / base) * 100;
// Strengste bodem van de collecties waar het product in zit.
let rule: GuardrailCollection | null = null;
for (const m of line.merchandise.product.inCollections) {
if (!m.isMember) continue;
const r = byCollection.get(m.collectionId);
if (r && (!rule || r.max_pct < rule.max_pct)) rule = r;
}
const max = rule ? rule.max_pct : fallback;
if (max == null) continue;
if (pct <= max + 0.05) continue;
const key = rule ? rule.id : "__default__";
if (reported.has(key)) continue;
reported.add(key);
const where = rule?.title ? `op ${rule.title}` : "in deze shop";
const message =
config.message?.trim() ||
`Kortingscodes gelden op ${line.merchandise.product.title} niet bovenop de lopende actie (maximaal ${fmt(max)}% korting ${where}, nu ${fmt(pct)}%). Haal de code weg om af te rekenen.`;
errors.push({ message, target: "$.cart" });
}Zakt een regel eronder, dan gaat de checkout dicht met een boodschap die de klant vertelt wat te doen: een code weghalen. Twee valkuilen uit de praktijk. De collectie-id's staan statisch in de input query, want Functions-inputvariabelen kunnen geen shop-metafields lezen; een nieuwe collectie is dus een regel in de query, of je werkt met tags. En zet de bodem ruimer dan je eigen automatische stapel, anders blokkeer je klanten zonder dat er een code is om weg te halen. Op de demo-store stond hij eerst op 25, en 3× De Sweater met staffel, welkomstdeal en OG-voordeel zat al op 31,2%.
Het prijsbrein: codes die afknippen in plaats van weigeren
Een muur is geen prettige checkout. Dus voor de codes die we zelf beheren, haalden we de muur weg. De welkomstcode is geen native kortingsregel meer, maar een function-backed code: de code hangt aan een discount Function, en bij het afrekenen krijgt die Function de ingevoerde code mee. Het percentage per code staat in hetzelfde json-veld als de bodem.
Het brein rekent per regel de automatische stapel na, met exact dezelfde shop-velden en regels als de losse Functions voor staffel, creator, member en OG. Dan weet hij hoeveel ruimte er onder de bodem nog is, en geeft de code precies dat. Geen foutmelding, geen geweigerde code. De klant krijgt wat er kan en leest waarom niet meer.
Shopify telt productkortingen op op de regelprijs en orderkortingen op wat daarna overblijft: effectief = 1 - (1 - P - x) * (1 - O) P = som productkortingen, O = som orderkortingen, x = de code, B = de bodem. Ruimte voor de code: x <= (1 - P) - (1 - B) / (1 - O) Demo: P = 0,15 (staffel), O = 0,20 (welkomstdeal 10% + OG 10%), B = 0,35 x <= 0,85 - 0,65 / 0,80 = 0,85 - 0,8125 = 0,0375 → 3,75%
De scene op de demo-store: 3× De Sweater, staffel 15%, welkomstdeal 10%, OG-voordeel 10%. Zonder brein zou WELKOM10 daar 40,5% korting op een trui maken. Met brein geeft de code 3,75% en staat er in de checkout: "Welkomstcode · 3,75% extra, tot het maximum van 35% korting op Truien & Knitwear". Op een shirt zonder lopende actie gewoon de volle 10%.
45 regels codeToon code +Verberg code −
// extensions/pricing-brain/src/cart_lines_discounts_generate_run.ts (kern)
const code = (input.triggeringDiscountCode ?? '').toUpperCase();
if (!code) return { operations: [] };
const guardrails = parseGuardrails(input.shop.guardrails?.jsonValue);
const codeCfg = guardrails.codes[code];
if (!codeCfg) return { operations: [] };
// De automatische stapel narekenen met dezelfde shop-velden als de losse Functions:
// P = productkortingen (staffel + creator), O = orderkortingen (member/welkomst + OG).
const staffel = staffelPercent(input.shop.tiers?.jsonValue, eligibleQty);
const creator = creatorPercent(input.shop.deals?.jsonValue, String(input.cart.bron?.value ?? '').toLowerCase());
const customer = input.cart.buyerIdentity?.customer;
const orderPct = memberPercent(input.shop.memberConfig?.jsonValue, customer) + ogPercent(input.shop.ogConfig?.jsonValue, customer?.createdAt);
const O = Math.min(orderPct, 99.9) / 100;
for (const line of lines) {
if (line.merchandise.__typename !== 'ProductVariant') continue;
const P = (isPreorder(line) ? 0 : staffel + creator) / 100;
let rule: { title?: string; max_pct: number } | null = null;
for (const m of line.merchandise.product.inCollections) {
const r = m.isMember ? guardrails.collections.get(m.collectionId) : undefined;
if (r && (!rule || r.max_pct < rule.max_pct)) rule = r;
}
const bodem = rule ? rule.max_pct : guardrails.default_max_pct;
let x = codeCfg.percent;
let capped = false;
if (bodem !== null) {
const room = ((1 - P) - (1 - bodem / 100) / (1 - O)) * 100;
if (room < x) { x = Math.max(0, Math.floor(room * 100 + 1e-6) / 100); capped = true; }
}
if (x <= 0) continue;
// ... regels met hetzelfde resultaat groeperen tot één kortingskandidaat
}
const label = codeCfg.label ?? code;
const candidates = [...groups.values()].map((g) => ({
message: g.capped
? `${label} · ${fmt(g.percent)}% extra, tot het maximum van ${fmt(g.bodem ?? 0)}% korting${g.title ? ` op ${g.title}` : ''}`
: `${label} · ${fmt(g.percent)}%`,
targets: g.lineIds.map((id) => ({ cartLine: { id } })),
value: { percentage: { value: g.percent } },
}));Waarom dat ertoe doet, met expliciete aannames: een trui van €120 met een inkoopprijs van €66. Bij 40% korting houd je €6 over, vóór verzending en retour. Bij 35% is dat €12. Die €6 verschil is precies wat de verzending kost.
Twee kanttekeningen. Dit werkt voor codes die door de Function lopen; een code uit het gewone kortingsscherm kan het brein niet afknippen, die vangt de kassa op met de bodem uit de vorige stap. En als je staffel- of member-regels veranderen, moet het brein mee. Zelfde velden, dus meestal alleen als de wiskunde verandert.
“De eigenaar bepaalt de bodem, niet de campagne. De bodem aanpassen is één getal in de admin.”
Wat je nog wél met een code doet
Codes zijn niet fout, ze zijn gereedschap voor een paar specifieke situaties. Een eenmalig excuus na een verkeerde levering: één code, één klant, één keer. Offline: de flyer in het pakket, de kaart op de beurs, de radiospot. Daar is geen link en geen account, dus daar hoort een code. En partners die je niet kunt of wilt koppelen aan een kanaal.
Voor die codes gelden dan wel regels. Een gebruikslimiet en een einddatum. Eén per klant waar het kan. Geen combinatie met lopende acties, of beter: door het brein laten lopen zodat ze afknippen in plaats van stapelen. En nooit meer een code als attributiesysteem voor een creator, want daar is hij nooit goed in geweest.
De volgorde die we aanhouden bij een nieuwe shop: eerst alle codes inventariseren en per code vragen waarvoor hij is. Creator of affiliate: link en Function. Vaste klanten: segment. Campagne voor iedereen: automatische korting, zichtbaar in de feed. Wat overblijft, krijgt een limiet, een einddatum en een bodem. Meestal blijft er dan verrassend weinig over.
Veelgestelde vragen.
- Werkt de kortingsbodem ook op kortingen van andere apps?
- Ja. De validation Function ziet per cart-regel de bedragen die erop liggen, ongeacht wie ze erop zette: automatische kortingen, codes, Functions van andere apps en de over regels verdeelde order-korting. De melding kan de code niet bij naam noemen, want de validation-input bevat alleen bedragen, geen codenamen.
- Heb je Shopify Plus nodig voor deze drie vervangers?
- Nee. De segment-korting is een admin-instelling op elk plan. Discount Functions en Cart and Checkout Validation Functions draaien ook op alle plannen. Alleen de checkout UI-blokken uit onze demo, zoals de dealbanner en het creator-bedankje, zijn checkout-extensies en die vragen Plus.
- Kan de bodem op marge in plaats van op een percentage?
- Ja, als de inkoopprijs als variant-metafield beschikbaar is. Dan wordt de bodem "minimaal X euro marge per regel" in plaats van een maximum percentage. De kostprijs uit Shopify zelf zit niet in de Function-input, dus die moet je als metafield meegeven.
Diek ThunnissenFounder & lead developer. Bouwt, verbetert en migreert de Shopify-laag voor DTC- en B2B-merken. LinkedInStuur me je checkout →

