Orders & Quotes
Quote requests
Submit a cart as a B2B quote request (RFQ), the quote status flow, and the customer-facing quote shape.
View as MarkdownThe quote (RFQ) flow lets a buyer submit a cart to the merchant for a priced offer instead of paying directly. It is only available when the store has the sales module enabled and quote requests switched on in its settings; on a store without either the endpoint returns 404. Certain carts must go through this flow: when a quote trigger fires, checkout refuses with 409 QUOTE_REQUIRED and the cart's requires_quote field is true.
The quote (RFQ) flow lets a buyer submit a cart to the merchant for a priced offer instead of paying directly. It is only available when the store has the sales module enabled and quote requests switched on in its settings; on a store without either the endpoint returns 404. Certain carts must go through this flow: when a quote trigger fires, checkout refuses with 409 QUOTE_REQUIRED and the cart's requires_quote field is true.
Quote status flow
| Status | Meaning |
|---|---|
submitted | Created by this endpoint. The cart is converted; the merchant sees the request. |
offered | The merchant priced the lines and sent the offer. Totals are frozen and a 48-character accept token is emailed to the buyer (only its SHA-256 hash is stored server-side). |
accepted | The buyer accepted via token. An order was created at the quoted prices; converted_order_id is set. Terminal. |
declined | The buyer declined via token. The merchant can still re-offer or cancel. |
expired | An offered quote past its valid_until flips to expired lazily on the next token or merchant touch. |
cancelled | Cancelled by the merchant (possible from submitted, offered, declined, expired). Terminal. |
Accepting and declining happen through the token endpoints (see the Quote tokens page).
Quote status flow
| Status | Meaning |
|---|---|
submitted | Created by this endpoint. The cart is converted; the merchant sees the request. |
offered | The merchant priced the lines and sent the offer. Totals are frozen and a 48-character accept token is emailed to the buyer (only its SHA-256 hash is stored server-side). |
accepted | The buyer accepted via token. An order was created at the quoted prices; converted_order_id is set. Terminal. |
declined | The buyer declined via token. The merchant can still re-offer or cancel. |
expired | An offered quote past its valid_until flips to expired lazily on the next token or merchant touch. |
cancelled | Cancelled by the merchant (possible from submitted, offered, declined, expired). Terminal. |
Accepting and declining happen through the token endpoints (see the Quote tokens page).
Quote triggers
When the store has the sales module enabled and quote requests switched on, these conditions force a cart into the RFQ flow. Otherwise no trigger fires, requires_quote stays false, and checkout never refuses with QUOTE_REQUIRED. They surface live on every cart response as requires_quote and quote_reasons:
| Reason | Fires when |
|---|---|
cart_threshold | The cart total reaches the configured threshold amount, or the quantity sum reaches the threshold quantity. |
configurator_choice | A chosen configurator option choice is flagged as requiring a quote. |
customer_group | The customer's contact group is set to always quote (a sales setting on the group). |
product_purchasability | A product in the cart has purchasability quote. Such a product can still be added to a cart; the cart then reports this reason and goes through this endpoint instead of checkout. |
Submitting a quote request is also allowed for carts that do not trip any trigger; the buyer can always ask for an offer.
Quote triggers
When the store has the sales module enabled and quote requests switched on, these conditions force a cart into the RFQ flow. Otherwise no trigger fires, requires_quote stays false, and checkout never refuses with QUOTE_REQUIRED. They surface live on every cart response as requires_quote and quote_reasons:
| Reason | Fires when |
|---|---|
cart_threshold | The cart total reaches the configured threshold amount, or the quantity sum reaches the threshold quantity. |
configurator_choice | A chosen configurator option choice is flagged as requiring a quote. |
customer_group | The customer's contact group is set to always quote (a sales setting on the group). |
product_purchasability | A product in the cart has purchasability quote. Such a product can still be added to a cart; the cart then reports this reason and goes through this endpoint instead of checkout. |
Submitting a quote request is also allowed for carts that do not trip any trigger; the buyer can always ask for an offer.
The customer-facing quote shape
The quote request endpoint and both token read endpoints return the same envelope: {"data": {...quote...}}. This is the customer-facing serialization: merchant_note, cart_id, customer_id, and contact_id are never present, and the accept token (or its hash) is never exposed. The quote object also carries origin ("storefront" for a quote created here), intro, terms, signed_at, acceptance_mode, pdf_url and signed_pdf_url, documented on the Quote tokens page. Unlike cart and order responses, all quote amounts are bare integers in minor units, not money objects. Dates are ISO-8601 strings.
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | always | Quote id. |
number | string | always | Human-facing quote number, "Q-" plus a per-store integer, for example "Q-17". |
status | string | always | See the status table above. |
email | string | always | Requester email. |
name | string | always | Requester name. |
phone | string or null | always | Requester phone. |
billing_address | object or null | always | As submitted. |
shipping_address | object or null | always | As submitted. |
currency | string | always | EUR. |
customer_note | string or null | always | The buyer's note from the request. |
message_to_customer | string or null | always | The merchant's message, set when offering. |
valid_until | string (ISO-8601) or null | always | Offer expiry, set when offering. |
subtotal_amount | integer or null | always | Frozen at offer time; null while submitted. Minor units. |
tax_total_amount | integer or null | always | Frozen at offer time; null while submitted. |
shipping_total_amount | integer or null | always | Frozen at offer time; null while submitted. |
total_amount | integer or null | always | Frozen at offer time; null while submitted. |
tax_breakdown | array or null | always | Per-rate breakdown {"rate": float, "base": int, "tax": int}; null while submitted. |
converted_order_id | string (UUID) or null | always | The order created on acceptance, else null. |
submitted_at | string (ISO-8601) or null | always | When the request was submitted. |
offered_at | string (ISO-8601) or null | always | When the merchant sent the offer. |
accepted_at | string (ISO-8601) or null | always | When the buyer accepted. |
declined_at | string (ISO-8601) or null | always | When the buyer declined. |
cancelled_at | string (ISO-8601) or null | always | When the merchant cancelled. |
created_at | string (ISO-8601) or null | always | Row creation time. |
updated_at | string (ISO-8601) or null | always | Last update time. |
lines | array | always | Quote lines, see below. |
lines[]
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | always | Quote line id. |
product_id | string (UUID) or null | always | The product; null for custom lines the merchant added. |
variant_id | string (UUID) or null | always | The variant, when applicable. |
is_custom | boolean | always | True for merchant-added custom lines. |
title | string | always | Line title (product name snapshot, or free text for custom lines). |
sku | string or null | always | SKU snapshot. |
quantity | number | always | Quantity, copied from the cart line: a whole number for a product sold by the piece, up to three decimals for any other unit (for example 0.75). Merchant service and material lines may be fractional too. |
indicative_unit_price | integer or null | always | Unit price (minor units) snapshotted from the requester's pricing context at submit time. |
quoted_unit_price | integer or null | always | The merchant's quoted unit price; null until priced. |
tax_class_id | string (UUID) or null | always | Tax class the merchant assigned, if any. |
attributes | object or null | always | Configuration snapshot for configurable lines, else null. |
sort_order | integer | always | Display order. |
kind | string | always | product for lines created from a cart; merchant lines may be service, material, or other. |
unit | string or null | always | Unit label. A line created from a cart carries pcs for a product sold by the piece and the product's unit otherwise (kg, m2, hour, ...). |
description | string or null | always | Optional line-level note. |
tax_rate | string or null | always | Decimal tax rate snapshotted when the merchant sends the offer (for example "21.00"); null until then. |
unit_price | integer or null | always | Alias of quoted_unit_price. |
The customer-facing quote shape
The quote request endpoint and both token read endpoints return the same envelope: {"data": {...quote...}}. This is the customer-facing serialization: merchant_note, cart_id, customer_id, and contact_id are never present, and the accept token (or its hash) is never exposed. The quote object also carries origin ("storefront" for a quote created here), intro, terms, signed_at, acceptance_mode, pdf_url and signed_pdf_url, documented on the Quote tokens page. Unlike cart and order responses, all quote amounts are bare integers in minor units, not money objects. Dates are ISO-8601 strings.
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | always | Quote id. |
number | string | always | Human-facing quote number, "Q-" plus a per-store integer, for example "Q-17". |
status | string | always | See the status table above. |
email | string | always | Requester email. |
name | string | always | Requester name. |
phone | string or null | always | Requester phone. |
billing_address | object or null | always | As submitted. |
shipping_address | object or null | always | As submitted. |
currency | string | always | EUR. |
customer_note | string or null | always | The buyer's note from the request. |
message_to_customer | string or null | always | The merchant's message, set when offering. |
valid_until | string (ISO-8601) or null | always | Offer expiry, set when offering. |
subtotal_amount | integer or null | always | Frozen at offer time; null while submitted. Minor units. |
tax_total_amount | integer or null | always | Frozen at offer time; null while submitted. |
shipping_total_amount | integer or null | always | Frozen at offer time; null while submitted. |
total_amount | integer or null | always | Frozen at offer time; null while submitted. |
tax_breakdown | array or null | always | Per-rate breakdown {"rate": float, "base": int, "tax": int}; null while submitted. |
converted_order_id | string (UUID) or null | always | The order created on acceptance, else null. |
submitted_at | string (ISO-8601) or null | always | When the request was submitted. |
offered_at | string (ISO-8601) or null | always | When the merchant sent the offer. |
accepted_at | string (ISO-8601) or null | always | When the buyer accepted. |
declined_at | string (ISO-8601) or null | always | When the buyer declined. |
cancelled_at | string (ISO-8601) or null | always | When the merchant cancelled. |
created_at | string (ISO-8601) or null | always | Row creation time. |
updated_at | string (ISO-8601) or null | always | Last update time. |
lines | array | always | Quote lines, see below. |
lines[]
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | always | Quote line id. |
product_id | string (UUID) or null | always | The product; null for custom lines the merchant added. |
variant_id | string (UUID) or null | always | The variant, when applicable. |
is_custom | boolean | always | True for merchant-added custom lines. |
title | string | always | Line title (product name snapshot, or free text for custom lines). |
sku | string or null | always | SKU snapshot. |
quantity | number | always | Quantity, copied from the cart line: a whole number for a product sold by the piece, up to three decimals for any other unit (for example 0.75). Merchant service and material lines may be fractional too. |
indicative_unit_price | integer or null | always | Unit price (minor units) snapshotted from the requester's pricing context at submit time. |
quoted_unit_price | integer or null | always | The merchant's quoted unit price; null until priced. |
tax_class_id | string (UUID) or null | always | Tax class the merchant assigned, if any. |
attributes | object or null | always | Configuration snapshot for configurable lines, else null. |
sort_order | integer | always | Display order. |
kind | string | always | product for lines created from a cart; merchant lines may be service, material, or other. |
unit | string or null | always | Unit label. A line created from a cart carries pcs for a product sold by the piece and the product's unit otherwise (kg, m2, hour, ...). |
description | string or null | always | Optional line-level note. |
tax_rate | string or null | always | Decimal tax rate snapshotted when the merchant sends the offer (for example "21.00"); null until then. |
unit_price | integer or null | always | Alias of quoted_unit_price. |
POST /v1/api/commerce/carts/{id}/quote-request
Submits the cart as a quote request. Requires commerce:write, an active non-empty cart, and passes the cart ownership check. On success the cart becomes converted, a quote is created in status submitted with each cart line snapshotted at an indicative unit price from the requester's pricing context, a received-confirmation email is queued to the requester, and the customer-facing quote is returned with status 201.
Contact resolution: email and name are required overall, but each can come from either the request body or, when a valid X-Customer-Token is sent, the customer's profile. A valid token also links the quote to the customer.
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | yes | Cart id. Must be an active, non-empty cart. |
Body
All body fields are optional and nullable; email and name become required when the customer profile cannot supply them.
| Name | Type | Required | Description |
|---|---|---|---|
email | string | conditional | Valid email. Falls back to the token'd customer's profile email. |
name | string | conditional | Requester name. Falls back to the customer profile name. |
phone | string | no | Contact phone. |
billing_address | object | no | Free-form address object. |
shipping_address | object | no | Free-form address object. |
customer_note | string | no | Message to the merchant. |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
X-Customer-Token | string | no | If present it must resolve, else 401. Links the quote to the customer, supplies contact fallbacks, and selects the pricing context for indicative prices. |
After submission there is no storefront endpoint to poll the quote by id: the buyer receives the offer by email, and the token endpoints take over from there.
Errors
| Status | Body | When |
|---|---|---|
| 404 | {"error": "Module not enabled: sales"} | The store does not have the sales module enabled. Note the error key and the 404 status (other module gates answer 403). |
| 404 | {"message": "..."} | Quote requests are switched off for the store, or the cart is unknown, expired, or not active. |
| 401 | {"message": "Invalid customer token."} | X-Customer-Token present but unresolvable. |
| 422 | {"message": "...", "errors": {"email": ["Email is required."], "name": ["Name is required."]}} | Neither the body nor the customer profile supplies email or name. |
| 422 | {"message": "...", "errors": {"cart": ["Cart is empty."]}} | The cart has no lines. |
| 403 | ownership, scope, commerce module, or suspension body | See the Carts page. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |
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"
}
}'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();{
"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
}
}POST /v1/api/commerce/carts/{id}/quote-request
Submits the cart as a quote request. Requires commerce:write, an active non-empty cart, and passes the cart ownership check. On success the cart becomes converted, a quote is created in status submitted with each cart line snapshotted at an indicative unit price from the requester's pricing context, a received-confirmation email is queued to the requester, and the customer-facing quote is returned with status 201.
Contact resolution: email and name are required overall, but each can come from either the request body or, when a valid X-Customer-Token is sent, the customer's profile. A valid token also links the quote to the customer.
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | yes | Cart id. Must be an active, non-empty cart. |
Body
All body fields are optional and nullable; email and name become required when the customer profile cannot supply them.
| Name | Type | Required | Description |
|---|---|---|---|
email | string | conditional | Valid email. Falls back to the token'd customer's profile email. |
name | string | conditional | Requester name. Falls back to the customer profile name. |
phone | string | no | Contact phone. |
billing_address | object | no | Free-form address object. |
shipping_address | object | no | Free-form address object. |
customer_note | string | no | Message to the merchant. |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
X-Customer-Token | string | no | If present it must resolve, else 401. Links the quote to the customer, supplies contact fallbacks, and selects the pricing context for indicative prices. |
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"
}
}'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();{
"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
}
}After submission there is no storefront endpoint to poll the quote by id: the buyer receives the offer by email, and the token endpoints take over from there.
Errors
| Status | Body | When |
|---|---|---|
| 404 | {"error": "Module not enabled: sales"} | The store does not have the sales module enabled. Note the error key and the 404 status (other module gates answer 403). |
| 404 | {"message": "..."} | Quote requests are switched off for the store, or the cart is unknown, expired, or not active. |
| 401 | {"message": "Invalid customer token."} | X-Customer-Token present but unresolvable. |
| 422 | {"message": "...", "errors": {"email": ["Email is required."], "name": ["Name is required."]}} | Neither the body nor the customer profile supplies email or name. |
| 422 | {"message": "...", "errors": {"cart": ["Cart is empty."]}} | The cart has no lines. |
| 403 | ownership, scope, commerce module, or suspension body | See the Carts page. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |