Carts & Checkout
Carts
Create, read, and update carts, including ownership token semantics and the full cart response shape.
View as MarkdownA cart is the mutable pre-order object a storefront builds up before checkout. Every cart belongs to one store, has a UUID id, and is always created in currency EUR with a 30-day expiry. Every write to a cart (adding lines, changing email, attaching a discount code) extends the expiry by another 30 days and updates the cart's last-activity timestamp. Expired carts are physically deleted; an expired cart id returns 404.
All cart endpoints require the Authorization: Bearer {api_key} header. Read endpoints need the commerce:read scope, write endpoints need commerce:write, and the commerce module must be enabled for the store. The tenant rate limit of 120 requests per minute applies.
A cart is the mutable pre-order object a storefront builds up before checkout. Every cart belongs to one store, has a UUID id, and is always created in currency EUR with a 30-day expiry. Every write to a cart (adding lines, changing email, attaching a discount code) extends the expiry by another 30 days and updates the cart's last-activity timestamp. Expired carts are physically deleted; an expired cart id returns 404.
All cart endpoints require the Authorization: Bearer {api_key} header. Read endpoints need the commerce:read scope, write endpoints need commerce:write, and the commerce module must be enabled for the store. The tenant rate limit of 120 requests per minute applies.
Cart lifecycle and status
| Status | Meaning |
|---|---|
active | The cart accepts writes. This is the only status a cart is ever created in. |
converted | The cart became an order (checkout) or a quote (quote request). Read-only. |
merged | The cart was folded into another cart during a claim. Read-only. |
Write endpoints look the cart up with status = active, so a converted or merged cart id returns 404 on every write. The exceptions are GET /v1/api/commerce/carts/{id}, which returns a cart in any status, and POST /v1/api/commerce/carts/{id}/checkout, which returns the existing order for an already-converted cart (idempotent replay).
Cart lifecycle and status
| Status | Meaning |
|---|---|
active | The cart accepts writes. This is the only status a cart is ever created in. |
converted | The cart became an order (checkout) or a quote (quote request). Read-only. |
merged | The cart was folded into another cart during a claim. Read-only. |
Write endpoints look the cart up with status = active, so a converted or merged cart id returns 404 on every write. The exceptions are GET /v1/api/commerce/carts/{id}, which returns a cart in any status, and POST /v1/api/commerce/carts/{id}/checkout, which returns the existing order for an already-converted cart (idempotent replay).
Ownership: the X-Customer-Token header
Carts start anonymous. The optional X-Customer-Token header carries an opaque customer session token (issued by the customers module) and affects cart routes in three ways:
- Pricing. When present and valid, prices are resolved in that customer's pricing context (customer-group pricing, B2B price display). Without it, guest pricing applies.
- Ownership. If a cart's
customer_idis set and that customer is a real (non-guest) account, every cart-scoped request must carry anX-Customer-Tokenthat resolves to that same customer. Otherwise the request fails with403 {"message": "This cart belongs to a customer."}. Carts withcustomer_idnull stay open, and so do carts bound to a guest customer row created by email capture (guests have no token to present). If the customers module is unavailable, the check fails closed with the same 403. - Binding.
POST /v1/api/commerce/carts/{id}/claimrequires the header, and at checkout a valid token binds the resulting order to the customer.
A token that is present but does not resolve returns 401 {"message": "Invalid customer token."} on checkout, quote request, and order history, and a plain 401 on claim. On the other cart routes an unresolvable token simply falls back to guest pricing unless the cart is customer-bound (then the ownership 403 applies).
Ownership: the X-Customer-Token header
Carts start anonymous. The optional X-Customer-Token header carries an opaque customer session token (issued by the customers module) and affects cart routes in three ways:
- Pricing. When present and valid, prices are resolved in that customer's pricing context (customer-group pricing, B2B price display). Without it, guest pricing applies.
- Ownership. If a cart's
customer_idis set and that customer is a real (non-guest) account, every cart-scoped request must carry anX-Customer-Tokenthat resolves to that same customer. Otherwise the request fails with403 {"message": "This cart belongs to a customer."}. Carts withcustomer_idnull stay open, and so do carts bound to a guest customer row created by email capture (guests have no token to present). If the customers module is unavailable, the check fails closed with the same 403. - Binding.
POST /v1/api/commerce/carts/{id}/claimrequires the header, and at checkout a valid token binds the resulting order to the customer.
A token that is present but does not resolve returns 401 {"message": "Invalid customer token."} on checkout, quote request, and order history, and a plain 401 on claim. On the other cart routes an unresolvable token simply falls back to guest pricing unless the cart is customer-bound (then the ownership 403 applies).
The cart response shape
Every cart endpoint (and several cart-line and discount endpoints) returns the same envelope: {"data": {...cart...}}. Fields:
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | always | Cart id. Store it client-side; it is the visitor's handle. |
currency | string | always | Always EUR. |
status | string | always | active, converted, or merged. |
email | string or null | always | Email captured via PATCH /carts/{id}, else null. |
metadata | object or null | always | Free-form scalar map set via PATCH /carts/{id}, else null. |
discount_code | string or null | always | The attached code, stored verbatim, else null. |
discount_code_error | string or null | always | Null when the code applies (or no code is set). Otherwise one of invalid, expired, min_order, not_eligible, usage_limit. |
discounts | array | always | Applied discounts, each {id, name, code, kind, amount}. kind is one of order, products, shipping, bogo; code is null for automatic discounts; amount is an integer in minor units. |
policy | object | always | The store's cart policy, see below. |
requires_quote | boolean | always | True when the cart currently trips a quote trigger and checkout will refuse with QUOTE_REQUIRED. |
quote_reasons | array of strings | always | Active trigger reasons: cart_threshold, configurator_choice, customer_group, product_purchasability. Empty when requires_quote is false. |
lines | array | always | Cart lines, see below. |
totals | object | always | Cart totals, see below. |
policy
The store's cart policy, evaluated live from tenant settings. Storefronts should render against it (for example, hide prices for guests, require login before checkout).
| Name | Type | Required | Description |
|---|---|---|---|
identity_mode | string | always | guest_allowed, login_required, or guest_only. |
audience | string | always | both, b2b_only, or b2c_only. |
min_order_value_amount | integer or null | always | Minimum cart total in minor units required to check out, or null for no minimum. |
max_lines | integer | always | Maximum number of distinct lines. Default 100. |
max_quantity_per_line | integer | always | Maximum quantity per line. Default 100. |
pricing | object | always | Price display block for the current pricing context. |
pricing.guest_price_visibility | string | always | visible or hidden. |
pricing.vat_display | string | always | inc or ex. Resolves to ex automatically for B2B customers. |
pricing.show_original_price | boolean | always | Whether to show the pre-discount price next to a discounted one. |
pricing.show_tier_table | boolean | always | Whether to show quantity tier price tables. |
lines[]
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | always | Line id. |
product_id | string (UUID) | always | The product. |
variant_id | string (UUID) or null | always | The variant, when the product has variants. |
name | string | always | Product name snapshot taken when the line was added. |
sku | string or null | always | SKU snapshot. |
quantity | integer | always | Quantity, at least 1. |
unit_price_amount | integer | always | Unit price in minor units (cents). |
unit_price | object | always | The same unit price as a money object {"amount": int, "currency": "EUR"}. |
line_total | object | always | Line total money object, after discount allocation. |
attributes | object or null | always | Null for plain lines. For configurable products this holds the full configuration snapshot (dimensions, choices, breakdown, components, matrix). |
totals
All money values are objects {"amount": int, "currency": "EUR"} with amount in minor units. Whether prices include tax is a store setting (prices_include_tax, default true).
| Name | Type | Required | Description |
|---|---|---|---|
subtotal | money | always | Goods total excluding tax. |
tax_total | money | always | Total tax. |
total | money | always | Grand total. |
discount_total | money | always | Total goods discount applied. |
tax_breakdown | array | always | Per-rate breakdown, each {"rate": float, "base": int, "tax": int} with base and tax in minor units. |
The cart response shape
Every cart endpoint (and several cart-line and discount endpoints) returns the same envelope: {"data": {...cart...}}. Fields:
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | always | Cart id. Store it client-side; it is the visitor's handle. |
currency | string | always | Always EUR. |
status | string | always | active, converted, or merged. |
email | string or null | always | Email captured via PATCH /carts/{id}, else null. |
metadata | object or null | always | Free-form scalar map set via PATCH /carts/{id}, else null. |
discount_code | string or null | always | The attached code, stored verbatim, else null. |
discount_code_error | string or null | always | Null when the code applies (or no code is set). Otherwise one of invalid, expired, min_order, not_eligible, usage_limit. |
discounts | array | always | Applied discounts, each {id, name, code, kind, amount}. kind is one of order, products, shipping, bogo; code is null for automatic discounts; amount is an integer in minor units. |
policy | object | always | The store's cart policy, see below. |
requires_quote | boolean | always | True when the cart currently trips a quote trigger and checkout will refuse with QUOTE_REQUIRED. |
quote_reasons | array of strings | always | Active trigger reasons: cart_threshold, configurator_choice, customer_group, product_purchasability. Empty when requires_quote is false. |
lines | array | always | Cart lines, see below. |
totals | object | always | Cart totals, see below. |
policy
The store's cart policy, evaluated live from tenant settings. Storefronts should render against it (for example, hide prices for guests, require login before checkout).
| Name | Type | Required | Description |
|---|---|---|---|
identity_mode | string | always | guest_allowed, login_required, or guest_only. |
audience | string | always | both, b2b_only, or b2c_only. |
min_order_value_amount | integer or null | always | Minimum cart total in minor units required to check out, or null for no minimum. |
max_lines | integer | always | Maximum number of distinct lines. Default 100. |
max_quantity_per_line | integer | always | Maximum quantity per line. Default 100. |
pricing | object | always | Price display block for the current pricing context. |
pricing.guest_price_visibility | string | always | visible or hidden. |
pricing.vat_display | string | always | inc or ex. Resolves to ex automatically for B2B customers. |
pricing.show_original_price | boolean | always | Whether to show the pre-discount price next to a discounted one. |
pricing.show_tier_table | boolean | always | Whether to show quantity tier price tables. |
lines[]
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | always | Line id. |
product_id | string (UUID) | always | The product. |
variant_id | string (UUID) or null | always | The variant, when the product has variants. |
name | string | always | Product name snapshot taken when the line was added. |
sku | string or null | always | SKU snapshot. |
quantity | integer | always | Quantity, at least 1. |
unit_price_amount | integer | always | Unit price in minor units (cents). |
unit_price | object | always | The same unit price as a money object {"amount": int, "currency": "EUR"}. |
line_total | object | always | Line total money object, after discount allocation. |
attributes | object or null | always | Null for plain lines. For configurable products this holds the full configuration snapshot (dimensions, choices, breakdown, components, matrix). |
totals
All money values are objects {"amount": int, "currency": "EUR"} with amount in minor units. Whether prices include tax is a store setting (prices_include_tax, default true).
| Name | Type | Required | Description |
|---|---|---|---|
subtotal | money | always | Goods total excluding tax. |
tax_total | money | always | Total tax. |
total | money | always | Grand total. |
discount_total | money | always | Total goods discount applied. |
tax_breakdown | array | always | Per-rate breakdown, each {"rate": float, "base": int, "tax": int} with base and tax in minor units. |
POST /v1/api/commerce/carts
Creates a new empty cart. Requires commerce:write. There is no request body and no customer token is needed: carts start anonymous. The cart is created with currency EUR, status active, and a 30-day expires_at. Persist the returned id client-side (cookie or local storage); it is the only handle to the cart.
Parameters
None.
The response status is 201.
Errors
| Status | Body | When |
|---|---|---|
| 401 | {"message": "Unauthenticated."} | Missing or invalid API key. |
| 403 | {"message": "Invalid ability provided."} | Key lacks commerce:write. |
| 403 | {"error": "Module not enabled: commerce"} | Commerce module disabled for the store. |
| 403 | {"message": "Tenant suspended.", "code": "tenant_suspended"} | Store suspended. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |
curl -X POST https://api.borkol.com/v1/api/commerce/carts \
-H "Authorization: Bearer {api_key}"const res = await fetch("https://api.borkol.com/v1/api/commerce/carts", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` },
});
const { data: cart } = await res.json();
// persist cart.id{
"data": {
"id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
"currency": "EUR",
"status": "active",
"email": null,
"metadata": null,
"discount_code": null,
"discount_code_error": null,
"discounts": [],
"policy": {
"identity_mode": "guest_allowed",
"audience": "both",
"min_order_value_amount": null,
"max_lines": 100,
"max_quantity_per_line": 100,
"pricing": {
"guest_price_visibility": "visible",
"vat_display": "inc",
"show_original_price": false,
"show_tier_table": true
}
},
"requires_quote": false,
"quote_reasons": [],
"lines": [],
"totals": {
"subtotal": { "amount": 0, "currency": "EUR" },
"tax_total": { "amount": 0, "currency": "EUR" },
"total": { "amount": 0, "currency": "EUR" },
"discount_total": { "amount": 0, "currency": "EUR" },
"tax_breakdown": []
}
}
}POST /v1/api/commerce/carts
Creates a new empty cart. Requires commerce:write. There is no request body and no customer token is needed: carts start anonymous. The cart is created with currency EUR, status active, and a 30-day expires_at. Persist the returned id client-side (cookie or local storage); it is the only handle to the cart.
Parameters
None.
curl -X POST https://api.borkol.com/v1/api/commerce/carts \
-H "Authorization: Bearer {api_key}"const res = await fetch("https://api.borkol.com/v1/api/commerce/carts", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` },
});
const { data: cart } = await res.json();
// persist cart.id{
"data": {
"id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
"currency": "EUR",
"status": "active",
"email": null,
"metadata": null,
"discount_code": null,
"discount_code_error": null,
"discounts": [],
"policy": {
"identity_mode": "guest_allowed",
"audience": "both",
"min_order_value_amount": null,
"max_lines": 100,
"max_quantity_per_line": 100,
"pricing": {
"guest_price_visibility": "visible",
"vat_display": "inc",
"show_original_price": false,
"show_tier_table": true
}
},
"requires_quote": false,
"quote_reasons": [],
"lines": [],
"totals": {
"subtotal": { "amount": 0, "currency": "EUR" },
"tax_total": { "amount": 0, "currency": "EUR" },
"total": { "amount": 0, "currency": "EUR" },
"discount_total": { "amount": 0, "currency": "EUR" },
"tax_breakdown": []
}
}
}The response status is 201.
Errors
| Status | Body | When |
|---|---|---|
| 401 | {"message": "Unauthenticated."} | Missing or invalid API key. |
| 403 | {"message": "Invalid ability provided."} | Key lacks commerce:write. |
| 403 | {"error": "Module not enabled: commerce"} | Commerce module disabled for the store. |
| 403 | {"message": "Tenant suspended.", "code": "tenant_suspended"} | Store suspended. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |
GET /v1/api/commerce/carts/{id}
Fetches a cart with lines, live-evaluated discounts, totals, the store's cart policy block, and the current quote-trigger state. Requires commerce:read. Unlike write endpoints, this works for a cart in any status (active, converted, merged). Discounts and discount_code_error are re-evaluated on every read, so this endpoint is the source of truth for rendering the cart. Cart ownership is enforced (see above).
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | yes | Cart id. |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
X-Customer-Token | string | no | Required if the cart is bound to a non-guest customer. Also selects customer-group pricing for displayed prices. |
Errors
| Status | Body | When |
|---|---|---|
| 404 | {"message": "..."} | Unknown id, expired (pruned) cart, or a cart belonging to another store. |
| 403 | {"message": "This cart belongs to a customer."} | Cart is customer-bound and the token is missing or resolves to a different customer. |
| 403 | scope, module, or suspension body | See POST /carts. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |
curl https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4 \
-H "Authorization: Bearer {api_key}"{
"data": {
"id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
"currency": "EUR",
"status": "active",
"email": "jane@example.com",
"metadata": { "source": "web" },
"discount_code": "WELCOME10",
"discount_code_error": null,
"discounts": [
{
"id": "71c9e0aa-2f4b-4d31-9a77-0b1c2d3e4f5a",
"name": "Welcome 10%",
"code": "WELCOME10",
"kind": "order",
"amount": 450
}
],
"policy": {
"identity_mode": "guest_allowed",
"audience": "both",
"min_order_value_amount": null,
"max_lines": 100,
"max_quantity_per_line": 100,
"pricing": {
"guest_price_visibility": "visible",
"vat_display": "inc",
"show_original_price": false,
"show_tier_table": true
}
},
"requires_quote": false,
"quote_reasons": [],
"lines": [
{
"id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
"product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"variant_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"name": "Garden Chair",
"sku": "CHAIR-GRN-M",
"quantity": 2,
"unit_price_amount": 2250,
"unit_price": { "amount": 2250, "currency": "EUR" },
"line_total": { "amount": 4050, "currency": "EUR" },
"attributes": null
}
],
"totals": {
"subtotal": { "amount": 3347, "currency": "EUR" },
"tax_total": { "amount": 703, "currency": "EUR" },
"total": { "amount": 4050, "currency": "EUR" },
"discount_total": { "amount": 450, "currency": "EUR" },
"tax_breakdown": [{ "rate": 21, "base": 3347, "tax": 703 }]
}
}
}GET /v1/api/commerce/carts/{id}
Fetches a cart with lines, live-evaluated discounts, totals, the store's cart policy block, and the current quote-trigger state. Requires commerce:read. Unlike write endpoints, this works for a cart in any status (active, converted, merged). Discounts and discount_code_error are re-evaluated on every read, so this endpoint is the source of truth for rendering the cart. Cart ownership is enforced (see above).
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | yes | Cart id. |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
X-Customer-Token | string | no | Required if the cart is bound to a non-guest customer. Also selects customer-group pricing for displayed prices. |
curl https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4 \
-H "Authorization: Bearer {api_key}"{
"data": {
"id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
"currency": "EUR",
"status": "active",
"email": "jane@example.com",
"metadata": { "source": "web" },
"discount_code": "WELCOME10",
"discount_code_error": null,
"discounts": [
{
"id": "71c9e0aa-2f4b-4d31-9a77-0b1c2d3e4f5a",
"name": "Welcome 10%",
"code": "WELCOME10",
"kind": "order",
"amount": 450
}
],
"policy": {
"identity_mode": "guest_allowed",
"audience": "both",
"min_order_value_amount": null,
"max_lines": 100,
"max_quantity_per_line": 100,
"pricing": {
"guest_price_visibility": "visible",
"vat_display": "inc",
"show_original_price": false,
"show_tier_table": true
}
},
"requires_quote": false,
"quote_reasons": [],
"lines": [
{
"id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
"product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"variant_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"name": "Garden Chair",
"sku": "CHAIR-GRN-M",
"quantity": 2,
"unit_price_amount": 2250,
"unit_price": { "amount": 2250, "currency": "EUR" },
"line_total": { "amount": 4050, "currency": "EUR" },
"attributes": null
}
],
"totals": {
"subtotal": { "amount": 3347, "currency": "EUR" },
"tax_total": { "amount": 703, "currency": "EUR" },
"total": { "amount": 4050, "currency": "EUR" },
"discount_total": { "amount": 450, "currency": "EUR" },
"tax_breakdown": [{ "rate": 21, "base": 3347, "tax": 703 }]
}
}
}Errors
| Status | Body | When |
|---|---|---|
| 404 | {"message": "..."} | Unknown id, expired (pruned) cart, or a cart belonging to another store. |
| 403 | {"message": "This cart belongs to a customer."} | Cart is customer-bound and the token is missing or resolves to a different customer. |
| 403 | scope, module, or suspension body | See POST /carts. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |
PATCH /v1/api/commerce/carts/{id}
Updates the cart's email and/or metadata. Requires commerce:write; the cart must be active; ownership is enforced. Setting email lowercases it and, when the customers module is active, finds or creates a guest customer row and binds the cart to it (or binds to the token's account when a valid X-Customer-Token proves ownership). This binding is the basis for abandoned-cart recovery; a guest-bound cart remains accessible without a token.
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | yes | Cart id. |
Body
| Name | Type | Required | Description |
|---|---|---|---|
email | string | no | Valid email address, max 255 characters. Stored lowercased. |
metadata | object or null | no | Free-form map, max 16 keys. Every value must be a scalar (string, number, boolean) or null. Pass null to clear. |
Both fields are sometimes rules: omitting a field leaves it unchanged.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
X-Customer-Token | string | no | Required if the cart is bound to a non-guest customer. |
The response is the full cart envelope, identical in shape to GET /v1/api/commerce/carts/{id}, with status 200.
Errors
| Status | Body | When |
|---|---|---|
| 422 | {"message": "...", "errors": {"email": ["The email field must be a valid email address."]}} | Validation failure (invalid email, too many metadata keys, non-scalar metadata value). |
| 404 | {"message": "..."} | Cart unknown, expired, or not active. |
| 403 | {"message": "This cart belongs to a customer."} | Ownership check failed. |
| 403 | scope, module, or suspension body | See POST /carts. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |
curl -X PATCH https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4 \
-H "Authorization: Bearer {api_key}" \
-H "Content-Type: application/json" \
-d '{"email": "jane@example.com", "metadata": {"utm_source": "newsletter"}}'{
"data": {
"id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
"email": "jane@example.com",
"metadata": { "utm_source": "newsletter" },
"...": "same cart shape as GET /v1/api/commerce/carts/{id}"
}
}PATCH /v1/api/commerce/carts/{id}
Updates the cart's email and/or metadata. Requires commerce:write; the cart must be active; ownership is enforced. Setting email lowercases it and, when the customers module is active, finds or creates a guest customer row and binds the cart to it (or binds to the token's account when a valid X-Customer-Token proves ownership). This binding is the basis for abandoned-cart recovery; a guest-bound cart remains accessible without a token.
Parameters
Path
| Name | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | yes | Cart id. |
Body
| Name | Type | Required | Description |
|---|---|---|---|
email | string | no | Valid email address, max 255 characters. Stored lowercased. |
metadata | object or null | no | Free-form map, max 16 keys. Every value must be a scalar (string, number, boolean) or null. Pass null to clear. |
Both fields are sometimes rules: omitting a field leaves it unchanged.
Headers
| Name | Type | Required | Description |
|---|---|---|---|
X-Customer-Token | string | no | Required if the cart is bound to a non-guest customer. |
curl -X PATCH https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4 \
-H "Authorization: Bearer {api_key}" \
-H "Content-Type: application/json" \
-d '{"email": "jane@example.com", "metadata": {"utm_source": "newsletter"}}'{
"data": {
"id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
"email": "jane@example.com",
"metadata": { "utm_source": "newsletter" },
"...": "same cart shape as GET /v1/api/commerce/carts/{id}"
}
}The response is the full cart envelope, identical in shape to GET /v1/api/commerce/carts/{id}, with status 200.
Errors
| Status | Body | When |
|---|---|---|
| 422 | {"message": "...", "errors": {"email": ["The email field must be a valid email address."]}} | Validation failure (invalid email, too many metadata keys, non-scalar metadata value). |
| 404 | {"message": "..."} | Cart unknown, expired, or not active. |
| 403 | {"message": "This cart belongs to a customer."} | Ownership check failed. |
| 403 | scope, module, or suspension body | See POST /carts. |
| 429 | {"message": "Too Many Attempts."} | Rate limit exceeded. |