# Complete walkthrough

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](/nl/api/storefront-guide/architecture) 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:

| Scope | Nodig voor |
| --- | --- |
| `commerce:read` | Producten, categorieën, configurator-schema en -quotes, carts lezen, verzendmethoden, orders, offertes lezen via token |
| `commerce:write` | Carts aanmaken en muteren, checkout, offerteaanvragen, offerte accepteren/afwijzen |
| `customers:read` | Klantprofiel en adressen lezen (alleen als je accounts bouwt) |
| `customers:write` | Registratie, 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](/nl/api/getting-started/authentication) en [Errors en rate limits](/nl/api/getting-started/errors-and-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.

```javascript title="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();
```

```json title="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.

```javascript title="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
```

```json title="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](/nl/api/catalog/seo).
- 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](/nl/api/catalog/products) en [Categorieën](/nl/api/catalog/categories).

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

```javascript title="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();
```

```json title="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](/nl/api/configurator/index), [Configuratieschema](/nl/api/configurator/schema), [Prijsquote](/nl/api/configurator/quote).

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

```javascript title="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](/nl/api/carts-and-checkout/carts), [Cart lines](/nl/api/carts-and-checkout/cart-lines), [Een cart claimen](/nl/api/carts-and-checkout/claim).

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

```javascript title="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();
```

```json title="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](/nl/api/carts-and-checkout/discounts), [Verzendmethoden](/nl/api/carts-and-checkout/shipping-methods).

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

```javascript title="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](/nl/api/carts-and-checkout/checkout) en [Orders](/nl/api/orders-and-quotes/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).

```javascript title="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](/nl/api/orders-and-quotes/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.

```javascript title="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](/nl/api/orders-and-quotes/quote-requests) en [Quote tokens](/nl/api/orders-and-quotes/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.

```javascript title="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](/nl/api/customers/index), [Registratie en sessies](/nl/api/customers/accounts), [Profiel](/nl/api/customers/profile), [Adressen](/nl/api/customers/addresses), [Wachtwoordreset](/nl/api/customers/passwords).

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

```json title="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:

```javascript title="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](/nl/api/webhooks/index).

## Waar ga je hierna naartoe

- [Storefront-architectuur](/nl/api/storefront-guide/architecture): het server-side proxypatroon, caching, idempotentie en environment-conventies.
- [Errors en rate limits](/nl/api/getting-started/errors-and-rate-limits): de complete error-catalogus die je proxy moet mappen.
- [Beschikbaarheid](/nl/api/stock/availability): bulk-voorraadopvragingen, als `stock_status` op producten niet genoeg is.
- [Objects](/nl/api/objects/index): CMS-achtige contenttypes en records voor door de merchant beheerde pagina's.
