Bestellingen & offertes

Offerteaanvragen

Een winkelwagen indienen als B2B-offerteaanvraag (RFQ), de offertestatusflow, en de klantgerichte offertevorm.

Bekijk als Markdown

Met de offerteflow (RFQ) kan een koper een winkelwagen bij de merchant indienen voor een geprijsd aanbod in plaats van direct te betalen. Deze is alleen beschikbaar wanneer de winkel de sales-module heeft ingeschakeld en offerteaanvragen aan heeft staan in de instellingen; op een winkel zonder een van beide retourneert het endpoint 404. Bepaalde winkelwagens moeten via deze flow: wanneer een quote trigger afgaat, weigert de checkout met 409 QUOTE_REQUIRED en is het veld requires_quote van de winkelwagen true.

Offertestatusflow

StatusBetekenis
submittedAangemaakt door dit endpoint. De winkelwagen is geconverteerd; de merchant ziet de aanvraag.
offeredDe merchant heeft de regels geprijsd en het aanbod verzonden. Totalen zijn bevroren en een accept token van 48 tekens is per e-mail naar de koper gestuurd (alleen de SHA-256-hash ervan wordt server-side opgeslagen).
acceptedDe koper heeft geaccepteerd via het token. Er is een bestelling aangemaakt tegen de geoffreerde prijzen; converted_order_id is gezet. Terminaal.
declinedDe koper heeft afgewezen via het token. De merchant kan nog opnieuw een aanbod doen of annuleren.
expiredEen aangeboden offerte waarvan valid_until is verstreken, klapt lazy om naar expired bij de eerstvolgende token- of merchant-actie.
cancelledGeannuleerd door de merchant (mogelijk vanuit submitted, offered, declined, expired). Terminaal.

Accepteren en afwijzen gebeurt via de token-endpoints (zie de pagina Offertetokens).

Quote triggers

Wanneer de winkel de sales-module heeft ingeschakeld en offerteaanvragen aan heeft staan, dwingen deze condities een winkelwagen in de RFQ-flow. Anders gaat er geen trigger af, blijft requires_quote false en weigert de checkout nooit met QUOTE_REQUIRED. Ze verschijnen live op elke winkelwagenrespons als requires_quote en quote_reasons:

RedenGaat af wanneer
cart_thresholdHet winkelwagentotaal bereikt het geconfigureerde drempelbedrag, of de som van de hoeveelheden bereikt de drempelhoeveelheid.
configurator_choiceEen gekozen configurator-optiekeuze is gemarkeerd als offerteplichtig.
customer_groupDe contactgroep van de klant staat op altijd offreren (een sales-instelling op de groep).
product_purchasabilityEen product in de winkelwagen heeft purchasability quote. Zo'n product kan nog steeds aan een winkelwagen worden toegevoegd; de winkelwagen meldt dan deze reden en gaat via dit endpoint in plaats van de checkout.

Een offerteaanvraag indienen is ook toegestaan voor winkelwagens die geen enkele trigger raken; de koper kan altijd om een aanbod vragen.

De klantgerichte offertevorm

Het offerteaanvraag-endpoint en beide token-read-endpoints retourneren dezelfde envelope: {"data": {...quote...}}. Dit is de klantgerichte serialisatie: merchant_note, cart_id, customer_id en contact_id zijn nooit aanwezig, en het accept token (of de hash ervan) wordt nooit blootgesteld. Het offerte-object bevat ook origin ("storefront" voor een offerte die hier is aangemaakt), intro, terms, signed_at, acceptance_mode, pdf_url en signed_pdf_url, gedocumenteerd op de pagina Offertetokens. Anders dan bij winkelwagen- en bestelresponses zijn alle offertebedragen kale integers in minor units, geen money-objecten. Datums zijn ISO-8601-strings.

NaamTypeVerplichtBeschrijving
idstring (UUID)altijdOfferte-id.
numberstringaltijdKlantgericht offertenummer, "Q-" plus een integer per winkel, bijvoorbeeld "Q-17".
statusstringaltijdZie de statustabel hierboven.
emailstringaltijdE-mailadres van de aanvrager.
namestringaltijdNaam van de aanvrager.
phonestring of nullaltijdTelefoonnummer van de aanvrager.
billing_addressobject of nullaltijdZoals ingediend.
shipping_addressobject of nullaltijdZoals ingediend.
currencystringaltijdEUR.
customer_notestring of nullaltijdDe notitie van de koper uit de aanvraag.
message_to_customerstring of nullaltijdHet bericht van de merchant, gezet bij het aanbieden.
valid_untilstring (ISO-8601) of nullaltijdVervaldatum van het aanbod, gezet bij het aanbieden.
subtotal_amountinteger of nullaltijdBevroren op het moment van aanbieden; null zolang submitted. Minor units.
tax_total_amountinteger of nullaltijdBevroren op het moment van aanbieden; null zolang submitted.
shipping_total_amountinteger of nullaltijdBevroren op het moment van aanbieden; null zolang submitted.
total_amountinteger of nullaltijdBevroren op het moment van aanbieden; null zolang submitted.
tax_breakdownarray of nullaltijdUitsplitsing per tarief {"rate": float, "base": int, "tax": int}; null zolang submitted.
converted_order_idstring (UUID) of nullaltijdDe bestelling die bij acceptatie is aangemaakt, anders null.
submitted_atstring (ISO-8601) of nullaltijdWanneer de aanvraag is ingediend.
offered_atstring (ISO-8601) of nullaltijdWanneer de merchant het aanbod heeft verzonden.
accepted_atstring (ISO-8601) of nullaltijdWanneer de koper heeft geaccepteerd.
declined_atstring (ISO-8601) of nullaltijdWanneer de koper heeft afgewezen.
cancelled_atstring (ISO-8601) of nullaltijdWanneer de merchant heeft geannuleerd.
created_atstring (ISO-8601) of nullaltijdAanmaaktijd van de rij.
updated_atstring (ISO-8601) of nullaltijdLaatste wijzigingstijd.
linesarrayaltijdOfferteregels, zie hieronder.

lines[]

NaamTypeVerplichtBeschrijving
idstring (UUID)altijdOfferteregel-id.
product_idstring (UUID) of nullaltijdHet product; null voor custom regels die de merchant heeft toegevoegd.
variant_idstring (UUID) of nullaltijdDe variant, indien van toepassing.
is_custombooleanaltijdTrue voor door de merchant toegevoegde custom regels.
titlestringaltijdRegeltitel (productnaam-snapshot, of vrije tekst voor custom regels).
skustring of nullaltijdSKU-snapshot.
quantitynumberaltijdHoeveelheid, overgenomen van de winkelwagenregel: een geheel getal voor een product dat per piece verkocht wordt, tot drie decimalen voor elke andere eenheid (bijvoorbeeld 0.75). Dienst- en materiaalregels van de merchant kunnen ook decimalen hebben.
indicative_unit_priceinteger of nullaltijdStukprijs (minor units), gesnapshot uit de prijscontext van de aanvrager op het moment van indienen.
quoted_unit_priceinteger of nullaltijdDe geoffreerde stukprijs van de merchant; null totdat er geprijsd is.
tax_class_idstring (UUID) of nullaltijdBtw-klasse die de merchant heeft toegewezen, indien aanwezig.
attributesobject of nullaltijdConfiguratie-snapshot voor configureerbare regels, anders null.
sort_orderintegeraltijdWeergavevolgorde.
kindstringaltijdproduct voor regels die uit een winkelwagen zijn gemaakt; regels van de merchant kunnen service, material of other zijn.
unitstring of nullaltijdEenheidslabel. Een regel uit een winkelwagen heeft pcs voor een product dat per piece verkocht wordt en anders de eenheid van het product (kg, m2, hour, ...).
descriptionstring of nullaltijdOptionele notitie op regelniveau.
tax_ratestring of nullaltijdDecimaal btw-tarief, vastgelegd wanneer de merchant het aanbod verstuurt (bijvoorbeeld "21.00"); tot dan null.
unit_priceinteger of nullaltijdAlias van quoted_unit_price.

POST /v1/api/commerce/carts/{id}/quote-request

Dient de winkelwagen in als offerteaanvraag. Vereist commerce:write, een active niet-lege winkelwagen, en doorloopt de eigenaarschapscontrole van de winkelwagen. Bij succes wordt de winkelwagen converted, wordt een offerte aangemaakt in status submitted waarbij elke winkelwagenregel wordt gesnapshot tegen een indicatieve stukprijs uit de prijscontext van de aanvrager, wordt een ontvangstbevestigingsmail voor de aanvrager in de wachtrij gezet, en wordt de klantgerichte offerte geretourneerd met status 201.

Contactresolutie: email en name zijn in totaal verplicht, maar elk kan komen uit ofwel de request body ofwel, wanneer een geldige X-Customer-Token wordt meegestuurd, het profiel van de klant. Een geldig token koppelt de offerte bovendien aan de klant.

Parameters

Pad

NaamTypeVerplichtBeschrijving
idstring (UUID)jaWinkelwagen-id. Moet een active, niet-lege winkelwagen zijn.

Body

Alle body-velden zijn optioneel en nullable; email en name worden verplicht wanneer het klantprofiel ze niet kan leveren.

NaamTypeVerplichtBeschrijving
emailstringconditioneelGeldig e-mailadres. Valt terug op het profiel-e-mailadres van de klant met token.
namestringconditioneelNaam van de aanvrager. Valt terug op de naam uit het klantprofiel.
phonestringneeContacttelefoonnummer.
billing_addressobjectneeVrij adresobject.
shipping_addressobjectneeVrij adresobject.
customer_notestringneeBericht aan de merchant.

Headers

NaamTypeVerplichtBeschrijving
X-Customer-TokenstringneeIndien aanwezig moet deze herleidbaar zijn, anders 401. Koppelt de offerte aan de klant, levert contact-fallbacks, en selecteert de prijscontext voor indicatieve prijzen.
Request
curl -X POST https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4/quote-request \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "buyer@corp.example",
    "name": "Bob Buyer",
    "phone": "+31 6 12345678",
    "customer_note": "Need delivery before October.",
    "billing_address": {
      "name": "Corp BV",
      "street": "Main St 1",
      "postal_code": "1000 AA",
      "city": "Amsterdam",
      "country": "NL"
    }
  }'
Request (JS)
const res = await fetch(
  `https://api.borkol.com/v1/api/commerce/carts/${cartId}/quote-request`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      email: "buyer@corp.example",
      name: "Bob Buyer",
      customer_note: "Need delivery before October.",
    }),
  }
);
const { data: quote } = await res.json();
Response
{
  "data": {
    "id": "6e7f8a9b-0c1d-4e2f-a3b4-c5d6e7f8a9b0",
    "number": "Q-17",
    "status": "submitted",
    "email": "buyer@corp.example",
    "name": "Bob Buyer",
    "phone": "+31 6 12345678",
    "billing_address": {
      "name": "Corp BV",
      "street": "Main St 1",
      "postal_code": "1000 AA",
      "city": "Amsterdam",
      "country": "NL"
    },
    "shipping_address": null,
    "currency": "EUR",
    "customer_note": "Need delivery before October.",
    "message_to_customer": null,
    "valid_until": null,
    "subtotal_amount": null,
    "tax_total_amount": null,
    "shipping_total_amount": null,
    "total_amount": null,
    "tax_breakdown": null,
    "converted_order_id": null,
    "submitted_at": "2026-08-14T10:15:30+00:00",
    "offered_at": null,
    "accepted_at": null,
    "declined_at": null,
    "cancelled_at": null,
    "created_at": "2026-08-14T10:15:30+00:00",
    "updated_at": "2026-08-14T10:15:30+00:00",
    "lines": [
      {
        "id": "9b0c1d2e-3f4a-4b5c-d6e7-f8a9b0c1d2e3",
        "product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        "variant_id": null,
        "is_custom": false,
        "title": "Garden Chair",
        "sku": "CHAIR-GRN",
        "quantity": 25,
        "indicative_unit_price": 2250,
        "quoted_unit_price": null,
        "tax_class_id": null,
        "attributes": null,
        "sort_order": 0,
        "kind": "product",
        "unit": "pcs",
        "description": null,
        "tax_rate": null,
        "unit_price": null
      }
    ],
    "origin": "storefront",
    "intro": null,
    "terms": null,
    "signed_at": null,
    "acceptance_mode": "click",
    "pdf_url": null,
    "signed_pdf_url": null
  }
}

Na het indienen is er geen storefront-endpoint om de offerte op id te pollen: de koper ontvangt het aanbod per e-mail, en de token-endpoints nemen het vanaf daar over.

Fouten

StatusBodyWanneer
404{"error": "Module not enabled: sales"}De winkel heeft de sales-module niet ingeschakeld. Let op de key error en de status 404 (andere module-gates antwoorden 403).
404{"message": "..."}Offerteaanvragen zijn uitgeschakeld voor de winkel, of de winkelwagen is onbekend, verlopen, of niet active.
401{"message": "Invalid customer token."}X-Customer-Token aanwezig maar niet herleidbaar.
422{"message": "...", "errors": {"email": ["Email is required."], "name": ["Name is required."]}}Noch de body noch het klantprofiel levert e-mailadres of naam.
422{"message": "...", "errors": {"cart": ["Cart is empty."]}}De winkelwagen heeft geen regels.
403eigenaarschaps-, scope-, commerce-module- of suspensie-bodyZie de pagina Winkelwagens.
429{"message": "Too Many Attempts."}Rate limit overschreden.