Orders & Quotes
Quote tokens
Fetch, accept, or decline an offered quote using the tokenized link from the offer email.
View as MarkdownWhen the merchant sends an offer, the buyer receives an email containing a plain 48-character accept token. Only the SHA-256 hash of that token is stored server-side, so the emailed link is the only credential; there is no way to re-derive it. The storefront hosts a quote page that takes the token from the link and drives these three endpoints. No X-Customer-Token is involved: the accept token itself is the proof of access.
All three endpoints lazily expire first: if the quote is offered and valid_until has passed, its status flips to expired before the request is processed. A read after expiry therefore returns "status": "expired", and accept or decline fail with QUOTE_NOT_OFFERED.
A token that does not match any quote of the store returns 404.
When the merchant sends an offer, the buyer receives an email containing a plain 48-character accept token. Only the SHA-256 hash of that token is stored server-side, so the emailed link is the only credential; there is no way to re-derive it. The storefront hosts a quote page that takes the token from the link and drives these three endpoints. No X-Customer-Token is involved: the accept token itself is the proof of access.
All three endpoints lazily expire first: if the quote is offered and valid_until has passed, its status flips to expired before the request is processed. A read after expiry therefore returns "status": "expired", and accept or decline fail with QUOTE_NOT_OFFERED.
A token that does not match any quote of the store returns 404.
GET /v1/api/commerce/quotes/by-token/{token}
Fetches the quote for this token. Requires commerce:read. Returns the customer-facing quote serialization (no merchant_note, cart_id, or customer_id; the token or its hash is never in the body). The full field reference is on the Quote requests page. Quote amounts are bare integers in minor units, not money objects.
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
token | string | yes | The plain 48-character token from the offer email. |
Errors
| Status | Body | When |
|---|---|---|
| 404 | {"message": "..."} | Unknown token, or a token belonging to another store. |
| 403 | scope, module, or suspension body | Key lacks commerce:read, module disabled, or store suspended. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |
curl https://api.borkol.com/v1/api/commerce/quotes/by-token/{token} \
-H "Authorization: Bearer {api_key}"{
"data": {
"id": "6e7f8a9b-0c1d-4e2f-a3b4-c5d6e7f8a9b0",
"number": "Q-17",
"status": "offered",
"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": "Volume price applied.",
"valid_until": "2026-08-31T23:59:59+00:00",
"subtotal_amount": 44421,
"tax_total_amount": 9329,
"shipping_total_amount": 0,
"total_amount": 53750,
"tax_breakdown": [{ "rate": 21, "base": 44421, "tax": 9329 }],
"converted_order_id": null,
"submitted_at": "2026-08-14T10:15:30+00:00",
"offered_at": "2026-08-15T09:00:00+00:00",
"accepted_at": null,
"declined_at": null,
"cancelled_at": null,
"created_at": "2026-08-14T10:15:30+00:00",
"updated_at": "2026-08-15T09:00:00+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": 2150,
"tax_class_id": null,
"attributes": null,
"sort_order": 0
}
]
}
}GET /v1/api/commerce/quotes/by-token/{token}
Fetches the quote for this token. Requires commerce:read. Returns the customer-facing quote serialization (no merchant_note, cart_id, or customer_id; the token or its hash is never in the body). The full field reference is on the Quote requests page. Quote amounts are bare integers in minor units, not money objects.
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
token | string | yes | The plain 48-character token from the offer email. |
curl https://api.borkol.com/v1/api/commerce/quotes/by-token/{token} \
-H "Authorization: Bearer {api_key}"{
"data": {
"id": "6e7f8a9b-0c1d-4e2f-a3b4-c5d6e7f8a9b0",
"number": "Q-17",
"status": "offered",
"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": "Volume price applied.",
"valid_until": "2026-08-31T23:59:59+00:00",
"subtotal_amount": 44421,
"tax_total_amount": 9329,
"shipping_total_amount": 0,
"total_amount": 53750,
"tax_breakdown": [{ "rate": 21, "base": 44421, "tax": 9329 }],
"converted_order_id": null,
"submitted_at": "2026-08-14T10:15:30+00:00",
"offered_at": "2026-08-15T09:00:00+00:00",
"accepted_at": null,
"declined_at": null,
"cancelled_at": null,
"created_at": "2026-08-14T10:15:30+00:00",
"updated_at": "2026-08-15T09:00:00+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": 2150,
"tax_class_id": null,
"attributes": null,
"sort_order": 0
}
]
}
}Errors
| Status | Body | When |
|---|---|---|
| 404 | {"message": "..."} | Unknown token, or a token belonging to another store. |
| 403 | scope, module, or suspension body | Key lacks commerce:read, module disabled, or store suspended. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |
POST /v1/api/commerce/quotes/by-token/{token}/accept
Accepts an offered quote, converting it into an order in status pending_payment at the quoted prices. Requires commerce:write. The operation is row-locked and idempotent: accepting an already-accepted quote returns the same order again. Before creating the order it re-computes the totals and refuses with OFFER_CHANGED if they no longer equal the figures frozen at send time (for example after a merchant re-edit or a tax change), and it checks and records stock for every non-custom product line, refusing with OUT_OF_STOCK on shortage. No request body.
The response is a bare object, not wrapped in data. order_number is a plain integer. Fetch the created order with GET /v1/api/commerce/orders/{order_id} if you need the full order.
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
token | string | yes | The plain token from the offer email. |
No body.
Errors
| Status | Body | When |
|---|---|---|
| 409 | {"code": "QUOTE_NOT_OFFERED", "message": "Quote cannot be accepted in its current status (declined)."} | The quote is not currently offered (submitted, declined, expired, or cancelled; the status in the message reflects the actual one). |
| 409 | {"code": "OFFER_CHANGED", "message": "This offer has changed since it was sent. Ask the merchant to re-send it."} | Recomputed totals no longer match the frozen offer figures. |
| 409 | {"code": "OUT_OF_STOCK", "out_of_stock": [{"product_id": "...", "variant_id": null, "requested": 25, "available": 10}]} | Insufficient stock for one or more product lines. |
| 404 | {"message": "..."} | Unknown token. |
| 403 | scope, module, or suspension body | Key lacks commerce:write, module disabled, or store suspended. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |
curl -X POST https://api.borkol.com/v1/api/commerce/quotes/by-token/{token}/accept \
-H "Authorization: Bearer {api_key}"const res = await fetch(
`https://api.borkol.com/v1/api/commerce/quotes/by-token/${token}/accept`,
{ method: "POST", headers: { Authorization: `Bearer ${apiKey}` } }
);
if (res.status === 409) {
const err = await res.json();
// err.code: "QUOTE_NOT_OFFERED" | "OFFER_CHANGED" | "OUT_OF_STOCK"
}
const { order_id, order_number } = await res.json();{
"order_id": "7f8a9b0c-1d2e-4f3a-b4c5-d6e7f8a9b0c1",
"order_number": 1043
}POST /v1/api/commerce/quotes/by-token/{token}/accept
Accepts an offered quote, converting it into an order in status pending_payment at the quoted prices. Requires commerce:write. The operation is row-locked and idempotent: accepting an already-accepted quote returns the same order again. Before creating the order it re-computes the totals and refuses with OFFER_CHANGED if they no longer equal the figures frozen at send time (for example after a merchant re-edit or a tax change), and it checks and records stock for every non-custom product line, refusing with OUT_OF_STOCK on shortage. No request body.
The response is a bare object, not wrapped in data. order_number is a plain integer. Fetch the created order with GET /v1/api/commerce/orders/{order_id} if you need the full order.
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
token | string | yes | The plain token from the offer email. |
No body.
curl -X POST https://api.borkol.com/v1/api/commerce/quotes/by-token/{token}/accept \
-H "Authorization: Bearer {api_key}"const res = await fetch(
`https://api.borkol.com/v1/api/commerce/quotes/by-token/${token}/accept`,
{ method: "POST", headers: { Authorization: `Bearer ${apiKey}` } }
);
if (res.status === 409) {
const err = await res.json();
// err.code: "QUOTE_NOT_OFFERED" | "OFFER_CHANGED" | "OUT_OF_STOCK"
}
const { order_id, order_number } = await res.json();{
"order_id": "7f8a9b0c-1d2e-4f3a-b4c5-d6e7f8a9b0c1",
"order_number": 1043
}Errors
| Status | Body | When |
|---|---|---|
| 409 | {"code": "QUOTE_NOT_OFFERED", "message": "Quote cannot be accepted in its current status (declined)."} | The quote is not currently offered (submitted, declined, expired, or cancelled; the status in the message reflects the actual one). |
| 409 | {"code": "OFFER_CHANGED", "message": "This offer has changed since it was sent. Ask the merchant to re-send it."} | Recomputed totals no longer match the frozen offer figures. |
| 409 | {"code": "OUT_OF_STOCK", "out_of_stock": [{"product_id": "...", "variant_id": null, "requested": 25, "available": 10}]} | Insufficient stock for one or more product lines. |
| 404 | {"message": "..."} | Unknown token. |
| 403 | scope, module, or suspension body | Key lacks commerce:write, module disabled, or store suspended. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |
POST /v1/api/commerce/quotes/by-token/{token}/decline
Declines an offered quote, with an optional reason that is appended to the merchant's internal note. Requires commerce:write. Row-locked against a racing accept. Returns the updated customer-facing quote (status declined, declined_at set) with status 200. A declined quote is not terminal: the merchant can still re-offer or cancel it.
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
token | string | yes | The plain token from the offer email. |
Body
| Name | Type | Required | Description |
|---|---|---|---|
reason | string or null | no | Why the buyer declined. Appended to the merchant's internal note; never echoed back in the customer-facing quote. |
Errors
| Status | Body | When |
|---|---|---|
| 409 | {"code": "QUOTE_NOT_OFFERED", "message": "Quote cannot be accepted in its current status (accepted)."} | The quote is not currently offered. |
| 422 | {"message": "...", "errors": {"reason": ["..."]}} | reason is present but not a string. |
| 404 | {"message": "..."} | Unknown token. |
| 403 | scope, module, or suspension body | Key lacks commerce:write, module disabled, or store suspended. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |
curl -X POST https://api.borkol.com/v1/api/commerce/quotes/by-token/{token}/decline \
-H "Authorization: Bearer {api_key}" \
-H "Content-Type: application/json" \
-d '{"reason": "Found a better price elsewhere."}'{
"data": {
"id": "6e7f8a9b-0c1d-4e2f-a3b4-c5d6e7f8a9b0",
"number": "Q-17",
"status": "declined",
"declined_at": "2026-08-16T08:30:00+00:00",
"...": "same customer-facing quote shape as GET /v1/api/commerce/quotes/by-token/{token}"
}
}POST /v1/api/commerce/quotes/by-token/{token}/decline
Declines an offered quote, with an optional reason that is appended to the merchant's internal note. Requires commerce:write. Row-locked against a racing accept. Returns the updated customer-facing quote (status declined, declined_at set) with status 200. A declined quote is not terminal: the merchant can still re-offer or cancel it.
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
token | string | yes | The plain token from the offer email. |
Body
| Name | Type | Required | Description |
|---|---|---|---|
reason | string or null | no | Why the buyer declined. Appended to the merchant's internal note; never echoed back in the customer-facing quote. |
curl -X POST https://api.borkol.com/v1/api/commerce/quotes/by-token/{token}/decline \
-H "Authorization: Bearer {api_key}" \
-H "Content-Type: application/json" \
-d '{"reason": "Found a better price elsewhere."}'{
"data": {
"id": "6e7f8a9b-0c1d-4e2f-a3b4-c5d6e7f8a9b0",
"number": "Q-17",
"status": "declined",
"declined_at": "2026-08-16T08:30:00+00:00",
"...": "same customer-facing quote shape as GET /v1/api/commerce/quotes/by-token/{token}"
}
}Errors
| Status | Body | When |
|---|---|---|
| 409 | {"code": "QUOTE_NOT_OFFERED", "message": "Quote cannot be accepted in its current status (accepted)."} | The quote is not currently offered. |
| 422 | {"message": "...", "errors": {"reason": ["..."]}} | reason is present but not a string. |
| 404 | {"message": "..."} | Unknown token. |
| 403 | scope, module, or suspension body | Key lacks commerce:write, module disabled, or store suspended. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |