Beschikbaar voor nieuwe Shopify-projecten

CheckoutBuild

M en L in één winkelwagen. Dus lieten we de checkout meekijken.

Diek ThunnissenDiek Thunnissen8 sep 20269 min leestijd

Bracketing: twee maten bestellen, thuis passen, één terugsturen. Met achteraf betalen kost het de klant niets en jou dubbele verzending. Een keten van vier platformonderdelen ziet het in de cart en past de checkout aan, zonder app.

Op elke fashion-webshop gebeurt het dagelijks: hetzelfde item in maat M en maat L in één winkelwagen. Eén van de twee gaat sowieso retour. Dat weet jij, en dat weet de klant ook.

Bracketing heet dat. Twee maten bestellen, thuis passen, één terugsturen. Met achteraf betalen kost het de klant helemaal niets. Jou kost het dubbele verzending, retourverwerking, en een item dat afgeprijsd terug het rek in gaat.

Tot voor kort kon je daar weinig mee. Je kon een retourbeleid schrijven, een maattabel toevoegen, of een app installeren die een popup toont. Allemaal aan de voorkant, allemaal vrijblijvend. De checkout zelf wist van niets.

Waarom de gebruikelijke oplossingen niets doen

Een strenger retourbeleid raakt ook je goede klanten. Een maattabel wordt niet gelezen door iemand die al besloten heeft twee maten te bestellen. Een retourkosten-app rekent achteraf af, als de schade al gedaan is. En al die middelen hebben één ding gemeen: ze weten niet wat er op dit moment in de winkelwagen ligt.

Het signaal is nochtans glashelder. Zelfde product, twee varianten die alleen in maat verschillen. Als je dát op het moment zelf kunt zien, kun je de checkout er ook op laten reageren.

De keten: vier onderdelen die sinds kort met elkaar praten

Stap één: sinds juni 2026 heeft elke Shopify-shop standaard storefront-events. Bij elke wijziging van de winkelwagen vuurt het thema een event met de volledige cart-inhoud. Een klein script luistert en ziet direct: zelfde product, twee maten.

Stap twee: dat script zet een cart attribute, een onzichtbaar veld op de winkelwagen. Dat kon altijd al via de Ajax-API, en sinds de standaard actielaag kan het in elk thema met dezelfde aanroep, zonder reload.

Stap drie: een Shopify Function leest dat attribute in de checkout en verbergt achteraf betalen voor precies deze bestelling. Server-side, dus niet te omzeilen door het script te blokkeren.

Stap vier: het attribute reist mee naar de order. Flow zet er een tag op, en je rapportage vertelt je voortaan hoe vaak zo'n maatduel-order daadwerkelijk retour komt.

Geen app, geen checkout-hack. Vier platformonderdelen die eindelijk met elkaar praten.

Het datamodel

Er is bijna geen datamodel, en dat is het punt. Eén cart attribute met een vaste key, gezet door het thema, gelezen door de Function, doorgegeven aan de order. Geen metafield, geen metaobject, geen database.

Cart attribute (gezet door het thema, leest de Function, landt op de order)

  key    _maatduel
  value  "overshirt-m-l"      (product-handle + de twee maten, voor je rapportage)

Order-tag (gezet door Flow)

  maatduel

De underscore in de key is een conventie die we overal aanhouden voor velden die alleen voor systemen bedoeld zijn. De waarde is bewust leesbaar: als je later in je orderoverzicht wilt weten wélke combinaties het vaakst voorkomen, staat het er gewoon.

Stap één en twee: het thema ziet het en zet het veld

Dit script luistert naar het standaard cart-event, haalt de actuele winkelwagen op en zoekt naar twee regels van hetzelfde product met een verschillende maat. Vindt hij die, dan zet hij het attribute. Verdwijnt de combinatie weer, dan haalt hij het weg. We gebruiken hier de Ajax-API voor het schrijven, omdat die in elk thema werkt en al jaren stabiel is.

42 regels codeToon code +
// assets/maatduel.js (laden in theme.liquid, defer)
(function () {
  const KEY = '_maatduel';
  const MAAT_OPTIES = ['maat', 'size', 'grootte'];

  function maatVan(item) {
    const optie = (item.options_with_values || []).find((o) =>
      MAAT_OPTIES.includes(String(o.name).toLowerCase())
    );
    return optie ? String(optie.value).toLowerCase() : null;
  }

  function vindMaatduel(cart) {
    const perProduct = new Map();
    for (const item of cart.items) {
      const maat = maatVan(item);
      if (!maat) continue;
      const set = perProduct.get(item.product_id) || { handle: item.handle, maten: new Set() };
      set.maten.add(maat);
      perProduct.set(item.product_id, set);
    }
    for (const [, p] of perProduct) {
      if (p.maten.size > 1) return p.handle + '-' + [...p.maten].sort().join('-');
    }
    return null;
  }

  async function sync() {
    const cart = await fetch('/cart.js').then((r) => r.json());
    const huidig = cart.attributes ? cart.attributes[KEY] || null : null;
    const nieuw = vindMaatduel(cart);
    if (huidig === nieuw) return;
    await fetch('/cart/update.js', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ attributes: { [KEY]: nieuw || '' } }),
    });
  }

  document.addEventListener('shopify:cart:lines-update', sync);
  document.addEventListener('DOMContentLoaded', sync);
})();

De lege string in plaats van null is bewust: zo verwijdert Shopify het attribute weer als de klant een van de twee maten uit de winkelwagen haalt. Anders blijft de vlag hangen en verbergt de Function achteraf betalen voor een bestelling waar niets meer mis mee is.

Stap drie: de Function die de betaalmethodes aanpast

Een Payment Customization Function. De input query vraagt het attribute op de cart en de lijst betaalmethodes. Staat het attribute erop, dan verdwijnt alles wat achteraf betalen is. iDEAL, kaarten en gewone Shop Pay blijven staan.

query CartPaymentMethodsTransformRunInput {
  cart {
    maatduel: attribute(key: "_maatduel") {
      value
    }
  }
  paymentMethods {
    id
    name
  }
}

De herkenning van achteraf betalen gaat op naam, want het input-object van een betaalmethode heeft geen type-veld. We gebruiken dezelfde lijst zoektermen als in onze pre-order-Function, die verbergt om dezelfde reden BNPL zodra er een pre-order in de cart ligt.

41 regels codeToon code +
import type {
  CartPaymentMethodsTransformRunInput,
  CartPaymentMethodsTransformRunResult,
} from "../generated/api";

// Zelfde lijst als extensions/preorder-payment: case-insensitive substring op de naam.
const BNPL_NEEDLES = [
  "klarna",
  "afterpay",
  "clearpay",
  "riverty",
  "billie",
  "scalapay",
  "in3",
  "achteraf",
  "pay later",
  "buy now pay later",
  "bnpl",
  "installment",
  "shop pay installment",
];

export function cartPaymentMethodsTransformRun(
  input: CartPaymentMethodsTransformRunInput,
): CartPaymentMethodsTransformRunResult {
  const vlag = input.cart.maatduel?.value;
  if (!vlag) {
    return { operations: [] };
  }

  const operations = input.paymentMethods
    .filter((method) => {
      const name = method.name.toLowerCase();
      return BNPL_NEEDLES.some((needle) => name.includes(needle));
    })
    .map((method) => ({
      paymentMethodHide: { paymentMethodId: method.id },
    }));

  return { operations };
}

Dit is de hele Function. Geen database, geen API-call, geen app die 's nachts iets synchroniseert. De beslissing valt op het moment dat de klant de checkout opent, op basis van wat er op dat moment in de winkelwagen ligt.

Stap vier: van order naar rapportage

Een cart attribute komt op de order terecht als custom attribute. Flow kan daarop filteren en een tag zetten. Dat is klikwerk in de Flow-editor, geen code.

Flow-workflow "Maatduel-order taggen"

  Trigger    Order created
  Conditie   order.customAttributes → any → key equals "_maatduel"
  Actie      Add order tags → maatduel

Vanaf dat moment kun je in je orderoverzicht filteren op de tag, en in je retourrapportage zien welk deel van de maatduel-orders daadwerkelijk terugkomt. Dat cijfer had je tot nu toe niet. Het vertelt je of de maatregel werkt, en of hij streng genoeg is, of juist te streng.

De valkuil: het thema moet de events vuren

De standaard storefront-events zijn een afspraak die het thema moet nakomen. Shopify's eigen thema's doen dat, veel andere thema's inmiddels ook, maar een ouder custom thema vuurt ze niet. Dan blijft je script wachten op een event dat nooit komt.

Daarom staat er in het script ook een sync bij het laden van de pagina. Dat vangt het grootste deel op: de klant komt vanuit de winkelwagen in de checkout, en op dat moment is het attribute in elk geval gezet. Wil je het waterdicht, dan roep je sync ook aan in je eigen add-to-cart-flow. Dat is één regel.

Wat de klant ziet

Niets bijzonders, en dat is de bedoeling. De winkelwagen werkt zoals altijd. In de checkout staan iDEAL en kaarten, en Klarna staat er deze keer niet tussen. Geen melding, geen uitleg, geen rood blok. De meeste klanten merken het niet eens; wie het merkt, bestelt de maat waarvan hij denkt dat hij past, en betaalt gewoon.

Wil je het wél uitleggen, dan zet je een klein checkout-blok naast de betaalmethodes: "Twee maten in je bestelling? Achteraf betalen is dan niet beschikbaar." Dat vraagt Plus voor het blok; de Function zelf niet.

Misschien te streng

Dat kan. Voor een merk dat leeft van goede service is achteraf betalen weghalen een stevige ingreep. Het mechanisme blijft dan hetzelfde, alleen de reactie verandert: in plaats van een betaalrestrictie toon je in de winkelwagen een maatadvies. "Twijfel je tussen M en L? Bij dit model kiezen de meeste klanten M." Het attribute is er al, het thema hoeft er alleen iets mee te doen.

Een lezer van de post wees op een juridisch punt dat we hier niet willen laten liggen: bij een consumentenkoop op afstand mag je in Nederland niet meer dan de helft vooruitbetaling verplicht stellen. Hoe zich dat verhoudt tot een checkout die voor deze ene bestelling alleen iDEAL en kaart aanbiedt, is een vraag voor je jurist, niet voor ons. Het maatadvies-alternatief heeft dat probleem niet.

En de dure variant, waar we uiteindelijk naartoe willen: de retourdata zelf laten meepraten. Per product en maat bijhouden hoe vaak hij terugkomt en waarom, dat als metafield op het product zetten, en de checkout laten zeggen "deze jas valt klein, de meeste klanten kiezen een maat groter", met een knop om ter plekke te wisselen. Zelfde keten, met geheugen.

Veelgestelde vragen.

Heb je Shopify Plus nodig om achteraf betalen te verbergen?
Nee. Payment Customization Functions draaien op elk plan. Alleen een eigen blok in de checkout (bijvoorbeeld een uitleg naast de betaalmethodes) is een checkout UI extension en die vraagt Plus.
Werkt dit als de klant JavaScript uitschakelt of het script blokkeert?
Dan wordt het attribute niet gezet en verbergt de Function niets. De keten begint in het thema, dus hij is zo betrouwbaar als het thema. Voor een maatadvies is dat prima. Wil je een harde regel die niemand kan omzeilen, dan moet de detectie in de Function zelf: die kan de cart-regels en hun varianten ook lezen en dezelfde vergelijking maken, zonder het thema.
Kun je dit ook voor kleur of andere opties doen?
Ja. Het script kijkt nu naar de optie die maat, size of grootte heet. Vervang die lijst door de optienaam die je wilt vergelijken. De rest van de keten verandert niet.
Diek ThunnissenDiek ThunnissenFounder & lead developer. Bouwt, verbetert en migreert de Shopify-laag voor DTC- en B2B-merken. LinkedInStuur me je checkout

Technische Shopify-inzichten uit de praktijk.

Geen sales, wel techniek.