Bouw een storefront

Complete walkthrough

Bouw een volledige custom storefront tegen de Borkol API, van API key tot catalogus, cart, checkout, orders, B2B-offertes, klantaccounts en webhooks.

Bekijk als Markdown

Deze pagina loopt door elke storefront-capaciteit van de Borkol API in de volgorde waarin je er een zou bouwen: catalogus browsen, productconfiguratie, carts, kortingen, checkout, ordertracking, de B2B-offerteflow, klantaccounts en webhooks. Elke stap geeft je werkende code en links naar de referentiepagina die elk veld documenteert. Met deze pagina plus de referentiehoofdstukken kun je een complete storefront bouwen zonder verder iets te lezen.

Base URL voor alle requests: https://api.borkol.com. Elke storefront-route leeft onder /v1/api en wordt geauthenticeerd met een tenant API key die je meestuurt als Authorization: Bearer {api_key}. De key identificeert de winkel, dus er is geen enkele tenant-header nodig. Alle geldbedragen zijn integers in minor units (centen) met een apart ISO 4217-valutaveld. Alle resource-ID's zijn UUID-strings (ordernummers zijn gewone integers). Timestamps zijn ISO 8601.

Eén regel voordat we code schrijven: de API key is een server-side secret. Hij autoriseert cart writes, checkout en order reads voor de hele winkel. Verscheep hem nooit naar een browser of mobiele app; zet een dunne server tussen de storefront-UI en de Borkol API. De architectuurpagina behandelt dit patroon in detail.

Stap 1: Haal een API key en scopes op

De merchant maakt API keys aan in het client dashboard (app.borkol.com) onder Settings > API keys. Bij het aanmaken kiest die een naam en een set scopes; de plaintext key wordt precies één keer getoond en kan daarna niet meer worden opgehaald. Keys verlopen nooit; ze werken totdat ze worden ingetrokken.

Scopes zijn read/write-paren per module. Een key kan alleen scopes dragen voor modules die op de winkel zijn ingeschakeld, plus tenant:read, die altijd toegekend kan worden. Vraag voor een volledige storefront een key aan met:

ScopeNodig voor
commerce:readProducten, categorieën, configurator-schema en -quotes, carts lezen, verzendmethoden, orders, offertes lezen via token
commerce:writeCarts aanmaken en muteren, checkout, offerteaanvragen, offerte accepteren/afwijzen
customers:readKlantprofiel en adressen lezen (alleen als je accounts bouwt)
customers:writeRegistratie, login, logout, profiel- en adres-writes (alleen als je accounts bouwt)

stock:read en stock:write zijn optioneel: productresponses bevatten al een stock_status-veld, dus de meeste storefronts roepen de stock-module nooit direct aan. objects:read is alleen nodig als de winkel de Objects-module gebruikt voor CMS-achtige content.

Een request met een key die de vereiste scope mist faalt met 403 {"message": "Invalid ability provided."}. Een request naar een module die de winkel niet heeft ingeschakeld faalt met 403 {"error": "Module not enabled: commerce"} (let op de error-key in die ene envelope). Alle details: Authenticatie en Errors en rate limits.

De rate limit is 120 requests per minuut per winkel, over alle keys van die winkel samen. Overschrijding retourneert 429 {"message": "Too Many Attempts."} met een Retry-After-header.

Stap 2: Health check en identiteit

GET /v1/ping is niet geauthenticeerd en verifieert de connectiviteit en de base URL. GET /v1/api/me is de canonieke eerste geauthenticeerde call: hij vereist geen scope, retourneert de winkel waar de key bij hoort en somt de scopes op die de key draagt. Roep hem aan bij het opstarten om snel te falen op een foute of ingetrokken key.

Request (JS)
const ping = await fetch("https://api.borkol.com/v1/ping");
// { "status": "ok", "version": "v1" }

const me = await fetch("https://api.borkol.com/v1/api/me", {
  headers: { Authorization: `Bearer ${process.env.BORKOL_API_KEY}` },
});
const identity = await me.json();
Response
{
  "tenant": {
    "id": "9c4e7f0a-2b31-4d6e-8f0a-1d2c3b4a5e6f",
    "name": "Four Seasons Garden",
    "slug": "four-seasons-garden"
  },
  "scopes": ["tenant:read", "commerce:read", "commerce:write", "customers:read", "customers:write"]
}

Een 401 {"message": "Unauthenticated."} betekent dat de key ontbreekt, misvormd is of is ingetrokken. Een 403 {"message": "Tenant suspended.", "code": "tenant_suspended"} betekent dat de winkel geschorst is; elke /v1/api-route retourneert dit zolang de schorsing duurt, behandel het dus als een storefront-brede outage-status.

Stap 3: Categorieën en producten ophalen, een catalogus renderen

Twee endpoints drijven de hele catalogus aan. GET /v1/api/commerce/categories retourneert de volledige actieve categorieboom, genest, gesorteerd op positie, niet gepagineerd. GET /v1/api/commerce/products retourneert actieve producten, nieuwste eerst, 20 per pagina (vast; er is geen per_page-, sorteer- of zoekparameter), optioneel gefilterd op een categorie-subtree met filter[category]={slug}. Een onbekende of inactieve slug retourneert een lege pagina, geen 404.

Request (JS)
const headers = { Authorization: `Bearer ${apiKey}` };

const cats = await fetch("https://api.borkol.com/v1/api/commerce/categories", { headers });
const { data: tree } = await cats.json();

const prods = await fetch(
  "https://api.borkol.com/v1/api/commerce/products?filter[category]=verandas&page=1",
  { headers }
);
const { data: products, meta } = await prods.json();
// meta.current_page, meta.last_page, meta.total drive your pager
Response (products, abridged)
{
  "data": [
    {
      "id": "9c2f1e6a-8b4d-4f2a-9e11-3d5a7b9c0e21",
      "capability": "standard",
      "capability_active": true,
      "purchasability": "direct",
      "unit": "piece",
      "sku": "TABLE-OAK",
      "slug": "oak-table",
      "name": "Oak Table",
      "description": "Solid oak table.",
      "status": "active",
      "price": { "amount": 54900, "currency": "EUR" },
      "price_display": { "mode": "inc" },
      "categories": [
        { "id": "7a1b3c5d-2e4f-4a6b-8c0d-1e2f3a4b5c6d", "name": "Tables", "slug": "tables", "is_active": true }
      ],
      "variants": [],
      "has_variants": false,
      "price_from": { "amount": 54900, "currency": "EUR" },
      "image": {
        "url": "https://cdn.borkol.com/t/9c2f/table.jpg",
        "variants": { "thumb": "https://wsrv.nl/?url=...&w=160&we=1&output=webp&q=80", "card": "https://wsrv.nl/?url=...&w=480&we=1&output=webp&q=80", "detail": "https://wsrv.nl/?url=...&w=1200&we=1&output=webp&q=80" },
        "alt": "Oak table"
      },
      "stock_status": "in_stock",
      "created_at": "2026-07-02T09:14:33.000000Z",
      "updated_at": "2026-08-01T15:20:11.000000Z"
    }
  ],
  "links": { "first": "...", "last": "...", "prev": null, "next": "..." },
  "meta": { "current_page": 1, "last_page": 3, "per_page": 20, "total": 47 }
}

Aandachtspunten bij het renderen:

  • Gebruik de image.variants-URL's (thumb 160w, card 480w, detail 1200w) voor responsive afbeeldingen; url is het origineel.
  • price.amount kan null zijn: winkels kunnen prijzen verbergen voor gasten. Render in dat geval "log in voor prijzen".
  • stock_status is een van in_stock, low_stock, out_of_stock, backorder, untracked, en is alleen aanwezig wanneer de stock-module is geregistreerd. untracked (het product heeft track_stock: false, of er wordt geen voorraad voor geteld) betekent dat het nooit uitverkocht raakt.
  • unit bepaalt het hoeveelheidsveld: een piece-product accepteert gehele getallen, elke andere eenheid (kg, m2, hour, ...) tot drie decimalen. capability vertelt je hoe je het aan de cart toevoegt (stap 5), en capability_active: false betekent dat het nu niet te koop is: verberg de koopknop.
  • Wanneer een product- of categorie-URL op je storefront niet meer bestaat, vraag dan GET /v1/api/redirects?path=... naar het nieuwe adres voordat je een 404 geeft, en bouw sitemap.xml op uit GET /v1/api/sitemap. Zie Redirects en sitemap.
  • De index-response laat options en de volledige images-galerij weg; haal GET /v1/api/commerce/products/{id} op voor de productdetailpagina, die beide toevoegt plus meta_title/meta_description voor SEO.
  • Producten met has_variants: true hebben een variantkiezer nodig, opgebouwd uit options (namen en waarden) gematcht tegen variants[].option_values.
  • Stuur het sessietoken van een ingelogde klant mee als X-Customer-Token op productrequests om de prijzen van die klant te krijgen (groepsprijzen, B2B-weergave ex btw, quantity_tiers). Zonder token wordt de prijs berekend voor een gast.

Veld-voor-veld referentie: Producten en Categorieën.

Stap 4: Configureerbare producten: schema en prijsquotes

Producten met capability: "made_to_measure" (een veranda, een op maat gemaakt zonnescherm) worden niet met een kaal product-ID aan de cart toegevoegd. Haal eerst het configuratieschema op, render daaruit een configurator-UI en beprijs vervolgens de selectie van de bezoeker met het quote-endpoint. Beide vereisen alleen catalog:read of commerce:read, en de winkel moet de configurator-module ingeschakeld hebben (anders 403 {"error": "Module not enabled: configurator"}).

GET /v1/api/commerce/configurator/products/{id}/schema retourneert drie blokken: dimensions (numerieke inputs met key, min/max en per keuze aangescherpte constraints), price_matrices (id, naam en de keuzecondities waaronder elke matrix geldt) en option_groups (single of multi select, required-flag, keuzes met prijsdelta's, requires/excludes-regels, afbeeldingen en requires_quote-flags). Het retourneert 404 tenzij het product actief en op maat gemaakt (made to measure) is.

POST /v1/api/commerce/configurator/products/{id}/quote beprijst één concrete selectie. Het is een pure berekening: er wordt niets aangemaakt, niets weggeschreven.

Request (JS)
const schemaRes = await fetch(
  `https://api.borkol.com/v1/api/commerce/configurator/products/${productId}/schema`,
  { headers }
);
const { data: schema } = await schemaRes.json();

const quoteRes = await fetch(
  `https://api.borkol.com/v1/api/commerce/configurator/products/${productId}/quote`,
  {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({
      dimensions: { width: 420, depth: 300 },
      choices: ["c1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c", "f6a7b8c9-d0e1-4f2a-b3c4-d5e6f7a8b9c0"],
    }),
  }
);
const { data: quote } = await quoteRes.json();
Response
{
  "data": {
    "total": 561500,
    "currency": "EUR",
    "breakdown": [
      { "label": "Base", "amount": 530000 },
      { "label": "LED lighting: LED set (6)", "amount": 31500 }
    ],
    "matrix": { "id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f7a", "name": "Glass roof" }
  }
}

Configurator-UI-regels die je frontend moet afdwingen (de API valideert ze allemaal opnieuw bij de quote, met één 422-melding per overtreden veld):

  • Elke dimensie in het schema is verplicht en moet een geheel getal binnen zijn min/max zijn. Het selecteren van een keuze die in de constraints van een dimensie staat scherpt dat bereik aan.
  • Hooguit één keuze per single-groep; elke required-groep moet gedekt zijn; requires-lijsten zijn OR binnen een doelgroep en AND over groepen heen; excludes zijn harde conflicten.
  • Vraag bij elke selectiewijziging een nieuwe quote aan zodat de getoonde prijs altijd live is. Debounce, maar cache niet agressief: quote-responses hangen af van de exacte selectie.
  • Een keuze met requires_quote: true betekent dat deze configuratie via de B2B-offerteflow (stap 9) moet lopen in plaats van directe checkout; het quote-endpoint beprijst hem nog steeds voor weergave.
  • Een 422 met errors.configuration = ["not_priceable"] betekent dat de merchant dit deel van de configuratieruimte niet heeft beprijsd. Toon een "offerte aanvragen"-fallback.

Volledig algoritme (matrixselectie, delta-passes, afronding) en elke 422-melding: Hoe de configurator werkt, Configuratieschema, Prijsquote.

Stap 5: Maak een cart en voeg regels toe

Carts zijn server-side objecten. Maak er een met POST /v1/api/commerce/carts (geen body; vereist commerce:write), bewaar de geretourneerde id voor de bezoeker (cookie of local storage gekoppeld aan je eigen sessie) en muteer hem met de regel-endpoints. Carts worden aangemaakt met valuta EUR, status active en een vervaltermijn van 30 dagen die bij elke write wordt verlengd.

Request (JS)
const cartRes = await fetch("https://api.borkol.com/v1/api/commerce/carts", {
  method: "POST",
  headers,
});
const { data: cart } = await cartRes.json(); // cart.id is the visitor's handle

// Standard product: product_id (+ variant_id when has_variants)
await fetch(`https://api.borkol.com/v1/api/commerce/carts/${cart.id}/lines`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    product_id: "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
    variant_id: "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
    quantity: 2,
  }),
});

// Made-to-measure product: configuration instead of variant_id
await fetch(`https://api.borkol.com/v1/api/commerce/carts/${cart.id}/lines`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    product_id: "9c2f1e6a-8b4d-4f2a-9e11-3d5a7b9c0e21",
    quantity: 1,
    configuration: {
      dimensions: { width: 420, depth: 300 },
      choices: ["c1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c"],
    },
  }),
});

Elke cart-mutatie retourneert de volledige cart (zelfde vorm als GET /v1/api/commerce/carts/{id}): regels met snapshots en geldobjecten, totals met tax_breakdown, het policy-blok van de tenant, requires_quote plus quote_reasons, en de kortingsstatus. Render je cart-UI opnieuw vanuit elke response; bereken totalen nooit client-side.

Regels die in de regel-endpoints zijn ingebakken:

  • variant_id is verplicht wanneer het product actieve varianten heeft en anders verboden. configuration is verplicht voor made_to_measure-producten en anders verboden. De 422-meldingen zijn expliciet ("Choose a variant for this product.", "Configure this product before adding it.") en bevatten een code op het hoogste niveau (CAPABILITY_INACTIVE, INVALID_CONFIGURATION, NOT_PURCHASABLE) om op te branchen.
  • quantity is een getal: geheel voor een piece-product, tot drie decimalen voor andere eenheden (0.75 kg). Cart-, order- en voorraadhoeveelheden komen terug als JSON-getallen.
  • Identiek product + variant + configuratie voegt samen met de bestaande regel: de hoeveelheid wordt opgeteld en de stukprijs wordt opnieuw bepaald op de nieuwe hoeveelheidstier.
  • PATCH .../lines/{lineId} met {"quantity": n} wijzigt de hoeveelheid; DELETE .../lines/{lineId} verwijdert de regel.
  • Het policy-blok draagt de limieten van de winkel: max_lines (standaard 100), max_quantity_per_line (standaard 100), min_order_value_amount, identity_mode (guest_allowed, login_required, guest_only) en audience. Limietovertredingen retourneren 422 {"message": "...", "code": "cart_constraint"}.
  • PATCH /v1/api/commerce/carts/{id} zet email en/of metadata (max 16 scalaire keys). Het zetten van het e-mailadres bindt de cart aan een gast-klantrij en maakt abandoned-cart recovery aan merchantzijde mogelijk.
  • Write-endpoints accepteren alleen carts met status active; een geconverteerde of samengevoegde cart retourneert 404 op writes (checkout is de enige uitzondering, zie stap 7).
  • Als een cart aan een echt klantaccount is gebonden, moet elk cart-request ook de X-Customer-Token van die klant meedragen, anders faalt het met 403 {"message": "This cart belongs to a customer."}.

Referentie: Carts, Cart lines, Een cart claimen.

Stap 6: Kortingscodes en verzendmethoden

Een cart draagt hooguit één kortingscode. PUT /v1/api/commerce/carts/{id}/discount-code met {"code": "WELCOME10"} slaat de code letterlijk op en geeft nooit een error voor een onbruikbare code: in plaats daarvan evalueert elke cart-response de code en rapporteert het resultaat. Controleer discount_code_error (null of een van invalid, expired, min_order, not_eligible, usage_limit) en render discounts[] (elk {id, name, code, kind, amount} met kind een van order, products, shipping, bogo). DELETE op dezelfde URL verwijdert de code.

Toon vóór de checkout de verzendopties met hun effectieve prijs voor deze specifieke cart:

Request (JS)
const shipRes = await fetch(
  `https://api.borkol.com/v1/api/commerce/carts/${cart.id}/shipping-methods`,
  { headers }
);
const { data: methods } = await shipRes.json();
Response
{
  "data": [
    { "id": "3c4d5e6f-7a8b-4c9d-0e1f-2a3b4c5d6e7f", "name": "Express", "amount": 995, "free_over_amount": null, "effective_amount": 995 },
    { "id": "4d5e6f7a-8b9c-4d0e-1f2a-3b4c5d6e7f8a", "name": "Standard", "amount": 495, "free_over_amount": 5000, "effective_amount": 0 }
  ]
}

effective_amount is wat deze cart daadwerkelijk gaat betalen: het valt terug naar 0 wanneer het goederentotaal van de cart de free_over_amount van de methode bereikt. Deze bedragen zijn kale integers in minor units, geen geldobjecten. Referentie: Kortingscodes, Verzendmethoden.

Stap 7: Checkout naar een order

POST /v1/api/commerce/carts/{id}/checkout converteert de cart naar een order met status pending_payment. Het valideert elke regel opnieuw, bepaalt elke prijs opnieuw, controleert de voorraad opnieuw, dwingt het cartbeleid van de winkel af, evalueert B2B-offertetriggers en vergrendelt het kortingsgebruik. shipping_method_id is verplicht precies wanneer de cart verzendbare regels bevat (producten met requires_shipping: true) en de winkel minstens één actieve verzendmethode heeft; carts met alleen producten zonder verzending slaan het over.

Request (JS)
const res = await fetch(`https://api.borkol.com/v1/api/commerce/carts/${cart.id}/checkout`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "jane@example.com",
    billing_address: {
      name: "Jane Doe",
      street: "Keizersgracht 1",
      postal_code: "1015 CC",
      city: "Amsterdam",
      country: "NL",
    },
    shipping_address: null,
    shipping_method_id: "4d5e6f7a-8b9c-4d0e-1f2a-3b4c5d6e7f8a",
  }),
});

if (res.status === 201 || res.status === 200) {
  const { data: order } = await res.json(); // order.id, order.number, order.status
} else if (res.status === 409) {
  const err = await res.json();
  if (err.code === "QUOTE_REQUIRED") {
    // route the buyer to the quote request flow (step 9); err.reasons explains why
  } else {
    // "Cart contents changed.": err.cart is the refreshed cart (lines carry
    // "unavailable": true where relevant), err.out_of_stock and
    // err.discount_unavailable list the specifics when applicable.
    // Re-render the cart from err.cart and let the buyer retry.
  }
} else if (res.status === 422) {
  const err = await res.json();
  // err.code (when present): login_required | guest_only | b2b_approval_required
  // | b2c_only | min_order_value | cart_constraint. Otherwise field validation
  // in err.errors ("Cart is empty.", "Choose a valid shipping method.", addresses).
}

Body-vereisten: email (verplicht, geldig e-mailadres), billing_address (verplicht object met verplichte name, street, postal_code, city en country als een exacte 2-tekens ISO-code), shipping_address (optioneel, nullable, vrije vorm), shipping_method_id (UUID, alleen verplicht onder de bovenstaande voorwaarde). De response is de volledige order: integer number, totalen als geldobjecten, tax_breakdown, btw per regel, verzendkosten en -naam.

Checkout is idempotent: opnieuw POSTen voor een al geconverteerde cart retourneert de bestaande order met 200 (het eerste succes is 201), dus een netwerk-retry kan nooit een dubbele order aanmaken. Als een geldige X-Customer-Token de checkout vergezelt, wordt de order aan die klant gebonden en verschijnt hij in diens ordergeschiedenis; een ongeldig token is een harde 401 {"message": "Invalid customer token."}.

Elke error-vorm en de volledige orderveldreferentie: Checkout en Orders.

Stap 8: Orderstatus en ordergeschiedenis

Orders bewegen door een vaste state machine, volledig aangestuurd door de merchant: pending_payment naar paid of cancelled; paid naar shipped, cancelled of refunded; shipped naar completed of refunded; completed naar refunded; cancelled en refunded zijn terminaal. De storefront leest alleen de status.

Twee leespaden:

  • GET /v1/api/commerce/orders/{id} haalt één order op via UUID met commerce:read. Er is geen check op klanteigenaarschap; de API key alleen autoriseert de call. Je storefront-server moet zelf verifiëren dat de aanvrager die order mag zien (bijvoorbeeld omdat hij uit de checkout van diens eigen sessie kwam) voordat je deze call proxied.
  • GET /v1/api/commerce/my/orders is de veilige "mijn orders"-pagina: hij vereist X-Customer-Token en retourneert alleen de orders van die klant, nieuwste eerst, gepagineerd (per_page 1 tot 100, standaard 25).
Request (JS)
// Confirmation page polling (your server verified ownership)
const orderRes = await fetch(`https://api.borkol.com/v1/api/commerce/orders/${orderId}`, { headers });
const { data: order } = await orderRes.json();
// order.status, order.tracking_carrier, order.tracking_number

// Account order history
const myOrders = await fetch("https://api.borkol.com/v1/api/commerce/my/orders?per_page=25", {
  headers: { ...headers, "X-Customer-Token": customerToken },
});
const { data: orders, meta } = await myOrders.json();

Elke paar seconden een orderstatuspagina pollen is verspilling van het budget van 120 requests per minuut; poll bij het laden van de pagina en na gebruikersactie, en gebruik webhooks (stap 11) om alles wat real-time moet zijn aan te sturen. Referentie: Orders.

Stap 9: Het B2B-pad: offerteaanvraag, accepteren en afwijzen via token

Winkels met de sales-module ingeschakeld en offerteaanvragen aan kunnen een RFQ-pad (request for quotation) afdwingen of aanbieden. De cart vertelt je wanneer: requires_quote: true met machine-leesbare quote_reasons (cart_threshold, configurator_choice, customer_group, product_purchasability). Checkout op zo'n cart weigert met 409 {"code": "QUOTE_REQUIRED", "reasons": [...]}, dus leid de koper naar een offerteaanvraagformulier in plaats van de betaalstap. Producten met purchasability: "quote" en configuratorkeuzes met de flag requires_quote horen daar direct vanaf de productpagina naartoe te routeren.

POST /v1/api/commerce/carts/{id}/quote-request dient de cart in als offerte (status submitted, nummer als Q-17) en converteert de cart. email en name zijn verplicht tenzij een geldige X-Customer-Token ze uit het klantprofiel aanlevert. Als de winkel offerteaanvragen heeft uitgeschakeld of de sales-module uit heeft staan, is het endpoint een 404.

Request (JS)
const rfq = await fetch(`https://api.borkol.com/v1/api/commerce/carts/${cart.id}/quote-request`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "buyer@corp.example",
    name: "Bob Buyer",
    customer_note: "Need delivery before October.",
  }),
});
const { data: quote } = await rfq.json(); // quote.number: "Q-17", quote.status: "submitted"

De merchant beprijst vervolgens de offerte en stuurt een aanbod. De koper ontvangt een e-mail met een link die een plain accept-token van 48 tekens bevat (alleen de SHA-256-hash ervan wordt server-side opgeslagen). Jouw storefront host de pagina waar die link naartoe wijst en stuurt drie token-endpoints aan; het token zelf is de credential, er komt geen X-Customer-Token aan te pas:

  • GET /v1/api/commerce/quotes/by-token/{token} haalt de klantgerichte offerte op (commerce:read): bevroren totalen als kale integer-bedragen, valid_until, regels met quoted_unit_price.
  • POST /v1/api/commerce/quotes/by-token/{token}/accept (commerce:write, geen body) converteert hem naar een order tegen de geoffreerde prijzen en retourneert een kale {"order_id": "...", "order_number": 1043}. Idempotent: nogmaals accepteren retourneert dezelfde order. Weigert met 409 QUOTE_NOT_OFFERED (niet actueel aangeboden, inclusief lazy verlopen na valid_until), OFFER_CHANGED (opnieuw berekende totalen matchen niet langer met het bevroren aanbod) of OUT_OF_STOCK (met een out_of_stock-array).
  • POST /v1/api/commerce/quotes/by-token/{token}/decline met een optionele {"reason": "..."} markeert hem als afgewezen.

Offertelevenscyclus: submitted naar offered (merchant verstuurt, totalen bevroren) naar accepted of declined; een aangeboden offerte voorbij valid_until klapt bij de eerstvolgende aanraking om naar expired; de merchant kan een cancel doen. Referentie: Offerteaanvragen en Quote tokens.

Stap 10: Klantaccounts

De Customers-module voegt registratie, login, profielen en adresboeken toe. Hij introduceert een tweede credential naast je API key: het klantsessietoken, geretourneerd door register en login en meegestuurd als de X-Customer-Token-header. De API key authenticeert je storefront-server bij de winkel; het klanttoken identificeert namens welke shopper het request handelt.

Request (JS)
const login = await fetch("https://api.borkol.com/v1/api/customers/login", {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({ email: "jane@example.com", password: "s3cret-pass" }),
});
const { data: customer, token: customerToken } = await login.json();

// Attach the anonymous cart to the account (merges any older active cart in)
await fetch(`https://api.borkol.com/v1/api/commerce/carts/${cart.id}/claim`, {
  method: "POST",
  headers: { ...headers, "X-Customer-Token": customerToken },
});

Wat het klanttoken over het hele platform doet:

  • Product- en cart-reads beprijzen voor de groep van de klant (tierprijzen, B2B-weergave ex btw).
  • POST /v1/api/commerce/carts/{id}/claim bindt een anonieme cart aan het account en voegt elke andere actieve cart van de klant erin samen.
  • Checkout met het token bindt de order aan het account, wat is wat GET /v1/api/commerce/my/orders vult.
  • Profiel en adressen leven onder GET/PATCH /v1/api/customers/me en GET/POST/PATCH/DELETE /v1/api/customers/me/addresses; gebruik het adresboek om de checkout voor in te vullen.

Registratie upgradet gastrijen in situ: als het e-mailadres al op een cart was vastgelegd (stap 5), behoudt het nieuwe account hetzelfde klant-id en de bijbehorende ordergeschiedenis. register en login dragen een extra limiet van 10 requests per minuut per IP bovenop de winkelbrede limiet, en beide vereisen customers:write. Bewaar het klanttoken server-side in je eigen sessie; het is een bearer credential.

Referentie: Klantaccounts, Registratie en sessies, Profiel, Adressen, Wachtwoordreset.

Stap 11: Webhooks voor order-events

Polling vertelt je wat er veranderd is wanneer jij het vraagt; webhooks vertellen het je op het moment dat het gebeurt. De merchant configureert endpoints in het client dashboard (Settings > Webhooks) met een URL, een lijst geabonneerde events en een secret met whsec_-prefix dat één keer wordt getoond bij het aanmaken. Borkol POST naar jouw endpoint met deze envelope:

Delivery body
{
  "event": "order.created",
  "created_at": "2026-08-14T09:12:44+00:00",
  "data": {
    "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "number": "1042",
    "status": "pending_payment",
    "total_amount": 15900,
    "currency": "EUR",
    "email": "buyer@example.com"
  }
}

Abonneerbare events: order.created en order.status_changed (beide met de order-datavorm hierboven) en stock.adjusted ({product_id, variant_id, quantity} met de nieuwe absolute hoeveelheid). Er bestaat een ping-event voor de "Send test"-knop in het dashboard.

Elke delivery draagt X-Borkol-Event (de eventnaam) en X-Borkol-Signature, een hex HMAC-SHA256 van de exacte raw request body, berekend met het endpoint-secret. Verifieer voordat je vertrouwt:

Signature verification (Node)
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, signatureHeader, secret) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(signatureHeader, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

Antwoord met een willekeurige 2xx binnen 10 seconden. Al het andere (of een timeout) wordt opnieuw geprobeerd: 3 pogingen in totaal met een backoff van 60 seconden en daarna 300 seconden, waarna de delivery als mislukt wordt gemarkeerd. Doe het echte werk (cache-invalidatie, e-mail, fulfilment-sync) na het antwoorden, op een queue. Typische storefront-toepassingen: de orderbevestigingspagina naar "paid" omzetten via order.status_changed, en een gecachte productpagina invalideren via stock.adjusted.

Volledige payload- en delivery-referentie: Webhooks.

Waar ga je hierna naartoe

  • Storefront-architectuur: het server-side proxypatroon, caching, idempotentie en environment-conventies.
  • Errors en rate limits: de complete error-catalogus die je proxy moet mappen.
  • Beschikbaarheid: bulk-voorraadopvragingen, als stock_status op producten niet genoeg is.
  • Objects: CMS-achtige contenttypes en records voor door de merchant beheerde pagina's.