Winkelwagens & Checkout
Checkout
Een actieve cart omzetten in een order, met handhaving van de cart policy, prijsafwijkingen en voorraadconflicten, quote-triggers en idempotente replay.
Bekijk als MarkdownCheckout zet een active cart om in een order met status pending_payment. Het is het strengste endpoint in de cart-flow: het hervalideert elke regel, bepaalt elke prijs opnieuw, controleert voorraad en gebruikslimieten van kortingen opnieuw, en dwingt de cart policy en quote-triggers van de store af. Een geslaagde checkout markeert de cart als converted en zet een orderbevestigingsmail in de wachtrij.
Checkout zet een active cart om in een order met status pending_payment. Het is het strengste endpoint in de cart-flow: het hervalideert elke regel, bepaalt elke prijs opnieuw, controleert voorraad en gebruikslimieten van kortingen opnieuw, en dwingt de cart policy en quote-triggers van de store af. Een geslaagde checkout markeert de cart als converted en zet een orderbevestigingsmail in de wachtrij.
POST /v1/api/commerce/carts/{id}/checkout
Vereist commerce:write. Wat er gebeurt, op volgorde:
- Eigenaarschap. Inline afgedwongen in plaats van via middleware, zodat idempotente replay van een reeds geconverteerde cart is vrijgesteld: een
activecart gebonden aan een niet-guest klant vereist de bijbehorendeX-Customer-Token, anders403 {"message": "This cart belongs to a customer."}. Guest-gebonden carts (via e-mailregistratie) blijven open. - Validatie. Adressen en, indien van toepassing, de verzendmethode worden gevalideerd (zie de body-tabel).
- Customer token. Als
X-Customer-Tokenaanwezig is moet het herleidbaar zijn, anders401 {"message": "Invalid customer token."}. Een geldig token koppelt de resulterende order aan die klant. - Quote-triggers. Als de store de sales-module heeft ingeschakeld met offerteaanvragen aan, en de cart een trigger activeert, weigert checkout met
409 {"code": "QUOTE_REQUIRED", "reasons": [...]}. De cart moet dan via de quote-request-flow. De cart-responseveldenrequires_quoteenquote_reasonsmaken dit vooraf zichtbaar. - Cart policy. De cart policy van de tenant wordt afgedwongen; overtredingen geven een 422 met een machineleesbare
code(zie fouten). - Voorraad. De voorraad wordt opnieuw gecontroleerd en bij de start van de checkout wordt een reservering (hold) vastgelegd voor maximaal 60 minuten; bij succes wordt de reservering omgezet in een echte afboeking.
- Herprijzen. Elke regel wordt gehervalideerd (product actief, capability nog actief, variant actief, configuratie nog geldig) en opnieuw geprijsd. Elke afwijking of onbeschikbaarheid breekt af met een 409 cart conflict; de ververste prijzen worden in de cart opgeslagen zodat de storefront opnieuw kan renderen en het opnieuw kan proberen.
- Kortingen. Gebruikslimieten worden onder een lock opnieuw gecontroleerd; een uitgeputte korting breekt af met een 409 cart conflict met
discount_unavailable. - Orderaanmaak. De order wordt aangemaakt met een per store oplopend integer
number, de cart wordtconverted, checkout-funnelgebeurtenissen worden geregistreerd, en een bevestigingsmail wordt in de wachtrij gezet.
Verzendvereiste. shipping_method_id is alleen verplicht wanneer beide voorwaarden gelden: de cart bevat minstens één regel waarvan het product requires_shipping: true heeft, en de store heeft minstens één actieve verzendmethode. Anders is het veld optioneel en wordt het genegeerd. Carts met uitsluitend producten zonder verzending (downloads, licenties, diensten) vereisen het nooit.
Idempotentie. Checkout opnieuw POSTen voor een reeds geconverteerde cart geeft de bestaande order terug met status 200 (een eerste succes geeft 201). Een unieke constraint op het cart-id vangt gelijktijdige dubbele submits af. Bij replay wordt de body nog steeds gevalideerd op de adresvelden, maar shipping_method_id is niet verplicht en de eigenaarschapscheck op het token wordt overgeslagen.
Parameters
Pad
| Naam | Type | Verplicht | Beschrijving |
|---|---|---|---|
id | string (UUID) | ja | Cart-id. |
Body
| Naam | Type | Verplicht | Beschrijving |
|---|---|---|---|
email | string | ja | Geldig e-mailadres voor de order. |
billing_address | object | ja | Factuuradres. |
billing_address.name | string | ja | Volledige naam. |
billing_address.street | string | ja | Straat en huisnummer. |
billing_address.postal_code | string | ja | Postcode. |
billing_address.city | string | ja | Plaats. |
billing_address.country | string | ja | Exact 2 tekens (ISO 3166-1 alpha-2). |
shipping_address | object of null | nee | Vrij adresobject; dezelfde vorm als billing_address wordt aanbevolen. Null of weggelaten betekent verzenden naar het factuuradres. |
shipping_method_id | string (UUID) | conditioneel | Verplicht wanneer de cart verzendbare regels heeft en de store minstens één actieve verzendmethode heeft. Moet het id van een actieve verzendmethode zijn. Anders optioneel en genegeerd. |
Headers
| Naam | Type | Verplicht | Beschrijving |
|---|---|---|---|
X-Customer-Token | string | nee | Indien aanwezig moet het herleidbaar zijn (anders 401). Koppelt de order aan de klant en is vereist wanneer de cart gebonden is aan een niet-guest klant. |
Status 201 bij een nieuwe order, 200 bij idempotente replay. De volledige veldreferentie van orders staat op de pagina Orders; de vorm hier is identiek.
De 409 cart-conflict-response
Wanneer het herprijzen een afwijking detecteert, een regel onbeschikbaar is geworden, er voorraadtekort is, of de gebruikslimiet van een korting is uitgeput, breekt checkout af met status 409:
cart is de volledige cart-resource met de ververste (al opgeslagen) prijzen, plus een extra boolean unavailable op elke regel. Een regel is onbeschikbaar wanneer hij niet meer te bepalen is: product ingetrokken, variant gedeactiveerd, configuratie niet meer geldig, of de capability niet meer actief. out_of_stock[].requested en .available zijn JSON-getallen en kunnen decimalen hebben voor producten die per gewicht, lengte, oppervlakte of volume verkocht worden. out_of_stock en discount_unavailable zijn alleen aanwezig wanneer ze van toepassing zijn. De juiste clientreactie is de cart opnieuw renderen vanuit cart, de klant laten bevestigen, en checkout opnieuw POSTen.
Fouten
| Status | Body | Wanneer |
|---|---|---|
| 422 | {"message": "...", "errors": {...}} | Validatiefout op adres of e-mail; ook Cart is empty. en Choose a valid shipping method. |
| 422 | {"message": "Log in to complete this order.", "code": "login_required"} | Overtreding van de cart policy. Andere codes: guest_only, b2b_approval_required, b2c_only, min_order_value. |
| 401 | {"message": "Invalid customer token."} | X-Customer-Token aanwezig maar niet herleidbaar. |
| 403 | {"message": "This cart belongs to a customer."} | Actieve klantgebonden cart zonder het token van de eigenaar. |
| 409 | {"code": "QUOTE_REQUIRED", "reasons": ["product_purchasability"]} | De cart moet via de quote-request-flow. Redenen: cart_threshold, configurator_choice, customer_group, product_purchasability. Alleen wanneer de store de sales-module heeft ingeschakeld met offerteaanvragen aan. |
| 409 | {"message": "Cart contents changed.", "cart": {...}, ...} | Prijsafwijking, onbeschikbare regel, voorraadtekort of uitgeputte korting (zie hierboven). |
| 404 | {"message": "..."} | Onbekend cart-id of een cart van een andere store. |
| 403 | scope-, module- of schorsingsbody | Zie de pagina Carts. |
| 429 | {"message": "Too Many Attempts."} | Rate limit overschreden. |
curl -X POST https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4/checkout \
-H "Authorization: Bearer {api_key}" \
-H "Content-Type: application/json" \
-d '{
"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"
}'const res = await fetch(
`https://api.borkol.com/v1/api/commerce/carts/${cartId}/checkout`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"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 === 409) {
const conflict = await res.json();
// conflict.code === "QUOTE_REQUIRED" or conflict.message === "Cart contents changed."
}
const { data: order } = await res.json();{
"data": {
"id": "7f8a9b0c-1d2e-4f3a-b4c5-d6e7f8a9b0c1",
"number": 1042,
"status": "pending_payment",
"email": "jane@example.com",
"billing_address": {
"name": "Jane Doe",
"street": "Keizersgracht 1",
"postal_code": "1015 CC",
"city": "Amsterdam",
"country": "NL"
},
"shipping_address": null,
"currency": "EUR",
"lines": [
{
"id": "8a9b0c1d-2e3f-4a4b-c5d6-e7f8a9b0c1d2",
"product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"variant_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"variant_label": "Green / M",
"name": "Garden Chair",
"sku": "CHAIR-GRN-M",
"quantity": 2,
"unit": "piece",
"unit_price": { "amount": 2250, "currency": "EUR" },
"line_subtotal": { "amount": 3347, "currency": "EUR" },
"tax_rate": 21,
"tax": { "amount": 703, "currency": "EUR" },
"line_total": { "amount": 4050, "currency": "EUR" },
"attributes": { "returnable": true, "lead_time_days": null }
}
],
"subtotal": { "amount": 3347, "currency": "EUR" },
"tax_total": { "amount": 789, "currency": "EUR" },
"total": { "amount": 4545, "currency": "EUR" },
"tax_breakdown": [{ "rate": 21, "base": 3756, "tax": 789 }],
"discount_total": { "amount": 0, "currency": "EUR" },
"discount_breakdown": null,
"shipping_method_name": "Standard",
"shipping": { "amount": 409, "currency": "EUR" },
"shipping_tax": { "amount": 86, "currency": "EUR" },
"tracking_carrier": null,
"tracking_number": null,
"invoice_number": null,
"invoiced_at": null,
"customer_id": null,
"contact_id": null,
"placed_at": "2026-08-14T10:15:30+00:00",
"created_at": "2026-08-14T10:15:30+00:00",
"invoice_id": null
}
}{
"message": "Cart contents changed.",
"cart": {
"id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
"lines": [
{
"id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
"unavailable": false,
"...": "same line shape as GET /v1/api/commerce/carts/{id}"
}
],
"...": "same cart shape as GET /v1/api/commerce/carts/{id}"
},
"out_of_stock": [
{
"product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"variant_id": null,
"requested": 2,
"available": 1
}
],
"discount_unavailable": [
{
"discount_id": "71c9e0aa-2f4b-4d31-9a77-0b1c2d3e4f5a",
"name": "Welcome 10%",
"code": "WELCOME10"
}
]
}POST /v1/api/commerce/carts/{id}/checkout
Vereist commerce:write. Wat er gebeurt, op volgorde:
- Eigenaarschap. Inline afgedwongen in plaats van via middleware, zodat idempotente replay van een reeds geconverteerde cart is vrijgesteld: een
activecart gebonden aan een niet-guest klant vereist de bijbehorendeX-Customer-Token, anders403 {"message": "This cart belongs to a customer."}. Guest-gebonden carts (via e-mailregistratie) blijven open. - Validatie. Adressen en, indien van toepassing, de verzendmethode worden gevalideerd (zie de body-tabel).
- Customer token. Als
X-Customer-Tokenaanwezig is moet het herleidbaar zijn, anders401 {"message": "Invalid customer token."}. Een geldig token koppelt de resulterende order aan die klant. - Quote-triggers. Als de store de sales-module heeft ingeschakeld met offerteaanvragen aan, en de cart een trigger activeert, weigert checkout met
409 {"code": "QUOTE_REQUIRED", "reasons": [...]}. De cart moet dan via de quote-request-flow. De cart-responseveldenrequires_quoteenquote_reasonsmaken dit vooraf zichtbaar. - Cart policy. De cart policy van de tenant wordt afgedwongen; overtredingen geven een 422 met een machineleesbare
code(zie fouten). - Voorraad. De voorraad wordt opnieuw gecontroleerd en bij de start van de checkout wordt een reservering (hold) vastgelegd voor maximaal 60 minuten; bij succes wordt de reservering omgezet in een echte afboeking.
- Herprijzen. Elke regel wordt gehervalideerd (product actief, capability nog actief, variant actief, configuratie nog geldig) en opnieuw geprijsd. Elke afwijking of onbeschikbaarheid breekt af met een 409 cart conflict; de ververste prijzen worden in de cart opgeslagen zodat de storefront opnieuw kan renderen en het opnieuw kan proberen.
- Kortingen. Gebruikslimieten worden onder een lock opnieuw gecontroleerd; een uitgeputte korting breekt af met een 409 cart conflict met
discount_unavailable. - Orderaanmaak. De order wordt aangemaakt met een per store oplopend integer
number, de cart wordtconverted, checkout-funnelgebeurtenissen worden geregistreerd, en een bevestigingsmail wordt in de wachtrij gezet.
Verzendvereiste. shipping_method_id is alleen verplicht wanneer beide voorwaarden gelden: de cart bevat minstens één regel waarvan het product requires_shipping: true heeft, en de store heeft minstens één actieve verzendmethode. Anders is het veld optioneel en wordt het genegeerd. Carts met uitsluitend producten zonder verzending (downloads, licenties, diensten) vereisen het nooit.
Idempotentie. Checkout opnieuw POSTen voor een reeds geconverteerde cart geeft de bestaande order terug met status 200 (een eerste succes geeft 201). Een unieke constraint op het cart-id vangt gelijktijdige dubbele submits af. Bij replay wordt de body nog steeds gevalideerd op de adresvelden, maar shipping_method_id is niet verplicht en de eigenaarschapscheck op het token wordt overgeslagen.
Parameters
Pad
| Naam | Type | Verplicht | Beschrijving |
|---|---|---|---|
id | string (UUID) | ja | Cart-id. |
Body
| Naam | Type | Verplicht | Beschrijving |
|---|---|---|---|
email | string | ja | Geldig e-mailadres voor de order. |
billing_address | object | ja | Factuuradres. |
billing_address.name | string | ja | Volledige naam. |
billing_address.street | string | ja | Straat en huisnummer. |
billing_address.postal_code | string | ja | Postcode. |
billing_address.city | string | ja | Plaats. |
billing_address.country | string | ja | Exact 2 tekens (ISO 3166-1 alpha-2). |
shipping_address | object of null | nee | Vrij adresobject; dezelfde vorm als billing_address wordt aanbevolen. Null of weggelaten betekent verzenden naar het factuuradres. |
shipping_method_id | string (UUID) | conditioneel | Verplicht wanneer de cart verzendbare regels heeft en de store minstens één actieve verzendmethode heeft. Moet het id van een actieve verzendmethode zijn. Anders optioneel en genegeerd. |
Headers
| Naam | Type | Verplicht | Beschrijving |
|---|---|---|---|
X-Customer-Token | string | nee | Indien aanwezig moet het herleidbaar zijn (anders 401). Koppelt de order aan de klant en is vereist wanneer de cart gebonden is aan een niet-guest klant. |
curl -X POST https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4/checkout \
-H "Authorization: Bearer {api_key}" \
-H "Content-Type: application/json" \
-d '{
"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"
}'const res = await fetch(
`https://api.borkol.com/v1/api/commerce/carts/${cartId}/checkout`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"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 === 409) {
const conflict = await res.json();
// conflict.code === "QUOTE_REQUIRED" or conflict.message === "Cart contents changed."
}
const { data: order } = await res.json();{
"data": {
"id": "7f8a9b0c-1d2e-4f3a-b4c5-d6e7f8a9b0c1",
"number": 1042,
"status": "pending_payment",
"email": "jane@example.com",
"billing_address": {
"name": "Jane Doe",
"street": "Keizersgracht 1",
"postal_code": "1015 CC",
"city": "Amsterdam",
"country": "NL"
},
"shipping_address": null,
"currency": "EUR",
"lines": [
{
"id": "8a9b0c1d-2e3f-4a4b-c5d6-e7f8a9b0c1d2",
"product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"variant_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"variant_label": "Green / M",
"name": "Garden Chair",
"sku": "CHAIR-GRN-M",
"quantity": 2,
"unit": "piece",
"unit_price": { "amount": 2250, "currency": "EUR" },
"line_subtotal": { "amount": 3347, "currency": "EUR" },
"tax_rate": 21,
"tax": { "amount": 703, "currency": "EUR" },
"line_total": { "amount": 4050, "currency": "EUR" },
"attributes": { "returnable": true, "lead_time_days": null }
}
],
"subtotal": { "amount": 3347, "currency": "EUR" },
"tax_total": { "amount": 789, "currency": "EUR" },
"total": { "amount": 4545, "currency": "EUR" },
"tax_breakdown": [{ "rate": 21, "base": 3756, "tax": 789 }],
"discount_total": { "amount": 0, "currency": "EUR" },
"discount_breakdown": null,
"shipping_method_name": "Standard",
"shipping": { "amount": 409, "currency": "EUR" },
"shipping_tax": { "amount": 86, "currency": "EUR" },
"tracking_carrier": null,
"tracking_number": null,
"invoice_number": null,
"invoiced_at": null,
"customer_id": null,
"contact_id": null,
"placed_at": "2026-08-14T10:15:30+00:00",
"created_at": "2026-08-14T10:15:30+00:00",
"invoice_id": null
}
}Status 201 bij een nieuwe order, 200 bij idempotente replay. De volledige veldreferentie van orders staat op de pagina Orders; de vorm hier is identiek.
De 409 cart-conflict-response
Wanneer het herprijzen een afwijking detecteert, een regel onbeschikbaar is geworden, er voorraadtekort is, of de gebruikslimiet van een korting is uitgeput, breekt checkout af met status 409:
{
"message": "Cart contents changed.",
"cart": {
"id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
"lines": [
{
"id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
"unavailable": false,
"...": "same line shape as GET /v1/api/commerce/carts/{id}"
}
],
"...": "same cart shape as GET /v1/api/commerce/carts/{id}"
},
"out_of_stock": [
{
"product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"variant_id": null,
"requested": 2,
"available": 1
}
],
"discount_unavailable": [
{
"discount_id": "71c9e0aa-2f4b-4d31-9a77-0b1c2d3e4f5a",
"name": "Welcome 10%",
"code": "WELCOME10"
}
]
}cart is de volledige cart-resource met de ververste (al opgeslagen) prijzen, plus een extra boolean unavailable op elke regel. Een regel is onbeschikbaar wanneer hij niet meer te bepalen is: product ingetrokken, variant gedeactiveerd, configuratie niet meer geldig, of de capability niet meer actief. out_of_stock[].requested en .available zijn JSON-getallen en kunnen decimalen hebben voor producten die per gewicht, lengte, oppervlakte of volume verkocht worden. out_of_stock en discount_unavailable zijn alleen aanwezig wanneer ze van toepassing zijn. De juiste clientreactie is de cart opnieuw renderen vanuit cart, de klant laten bevestigen, en checkout opnieuw POSTen.
Fouten
| Status | Body | Wanneer |
|---|---|---|
| 422 | {"message": "...", "errors": {...}} | Validatiefout op adres of e-mail; ook Cart is empty. en Choose a valid shipping method. |
| 422 | {"message": "Log in to complete this order.", "code": "login_required"} | Overtreding van de cart policy. Andere codes: guest_only, b2b_approval_required, b2c_only, min_order_value. |
| 401 | {"message": "Invalid customer token."} | X-Customer-Token aanwezig maar niet herleidbaar. |
| 403 | {"message": "This cart belongs to a customer."} | Actieve klantgebonden cart zonder het token van de eigenaar. |
| 409 | {"code": "QUOTE_REQUIRED", "reasons": ["product_purchasability"]} | De cart moet via de quote-request-flow. Redenen: cart_threshold, configurator_choice, customer_group, product_purchasability. Alleen wanneer de store de sales-module heeft ingeschakeld met offerteaanvragen aan. |
| 409 | {"message": "Cart contents changed.", "cart": {...}, ...} | Prijsafwijking, onbeschikbare regel, voorraadtekort of uitgeputte korting (zie hierboven). |
| 404 | {"message": "..."} | Onbekend cart-id of een cart van een andere store. |
| 403 | scope-, module- of schorsingsbody | Zie de pagina Carts. |
| 429 | {"message": "Too Many Attempts."} | Rate limit overschreden. |