Search Documentation

Search for a documentation page...

Carts API

REST API endpoints for managing shopping carts.

Add to Cart / Create Cart

POST /api/v1/carts

Creates a new cart or updates an existing one. To create a new cart, omit cartId. To add to an existing cart, include it.

The endpoint supports two mutually exclusive modes: variant add (variantId + quantity) and bundle add (bundleId + selections).

Variant Add

Add a single product variant to the cart. Setting quantity to 0 removes the item.

FieldTypeRequiredDescription
variantIdstringYesProduct variant UUID
quantitynumberYesQuantity to apply (0 removes the item)
modestringNo"add" (default) increments the existing line quantity; "set" replaces it atomically
cartIdstringNoExisting cart UUID (omit to create new)
subscriptionPlanIdstringNoSubscription plan ID for recurring items — see Subscription items
currencystringNoCurrency code for this cart (e.g. EUR). Validated against store's enabled currencies
localestringNoThe storefront language the shopper is browsing in, e.g. pl-PL. Must be the store's default language or one of its enabled languages (400 otherwise). Kept on the cart and copied to the order and the customer at checkout, so the hosted checkout, the payment form and every email about the order render in it. The last value sent wins
propertiesobject[]NoShopper personalisation for this line — see Line item properties
# Create a new cart
curl -X POST \
-H "Authorization: Bearer your_api_key" \
-H "Content-Type: application/json" \
-d '{
"variantId": "0191abc0-0000-7000-8000-000000000100",
"quantity": 1
}' \
https://your-store.yns.store/api/v1/carts
# Add to existing cart
curl -X POST \
-H "Authorization: Bearer your_api_key" \
-H "Content-Type: application/json" \
-d '{
"cartId": "0191abc0-1234-7def-8000-000000000001",
"variantId": "0191abc0-0000-7000-8000-000000000200",
"quantity": 2
}' \
https://your-store.yns.store/api/v1/carts

Line item properties

Attach personalisation to a line, such as an engraving or a photo for a made-to-order item. Pass properties with a variant add:

FieldTypeRequiredDescription
keystringYesStable machine key: 1–40 lowercase letters, digits, _ or - (e.g. engraving)
labelstringNoShopper-facing label, up to 80 characters
typestringYes"text" or "file"
valuestringYesThe text (up to 1,000 characters), or for file the uploadId of an uploaded file
curl -X POST \
-H "Authorization: Bearer your_api_key" \
-H "Content-Type: application/json" \
-d '{
"variantId": "0191abc0-0000-7000-8000-000000000100",
"quantity": 1,
"properties": [
{ "key": "engraving", "label": "Engraving", "type": "text", "value": "For Ana" }
]
}' \
https://your-store.yns.store/api/v1/carts

Rules:

  • Up to 10 entries with unique keys, at most 3 of type file, and 8 KB in total.
  • The same variant with the same properties stacks onto one line. Different properties make a separate line.
  • quantity: 0 removes only the line those properties identify.
  • All lines of a variant share its stock, so the 409 Insufficient stock check counts every line of the variant already in the cart.
  • Not combinable with bundleId or subscriptionPlanId. Invalid properties return 400.
  • Properties are copied onto the order's line items.

File properties: a file value is the uploadId returned by the storefront's POST /checkout/api/line-item-uploads endpoint. It requires the Customer Uploads module and is rate limited. Files go to private storage and are never publicly readable. The upload must belong to this store, must not be in another cart, and must really be the image or PDF it was uploaded as (JPEG, PNG, WebP, GIF, HEIC, AVIF, TIFF or PDF, up to 20 MB). Otherwise the add returns an error. Unused uploads are deleted after 24 hours, and a cart idle for 14 days loses its personalised lines and their files.

Bundle Add

Add a configured bundle as one line item. Get the available groups and choices from the bundle field on GET /api/v1/products/:idOrSlug for bundle-type products.

FieldTypeRequiredDescription
bundleIdstringYesBundle product UUID
selectionsobject[]YesCustomer choices (see below)
cartIdstringNoExisting cart UUID (omit to create new)
currencystringNoCurrency code for this cart
localestringNoThe storefront language, as for a variant add

Each entry in selections:

FieldTypeRequiredDescription
variantIdstringYesChosen variant UUID
groupIdstringYesThe bundle group the choice belongs to
quantitynumberYesUnits chosen of this variant (positive integer)

Always-included (forced) items may be omitted from selections – the server fills them in automatically. Choices are validated server-side against the bundle's groups (per-group count, allow-duplicates, stock).

curl -X POST \
-H "Authorization: Bearer your_api_key" \
-H "Content-Type: application/json" \
-d '{
"bundleId": "0191abc0-1234-7def-8000-000000000001",
"selections": [
{
"variantId": "0191abc0-0000-7000-8000-000000000100",
"groupId": "0191abc0-0000-7000-8000-000000000300",
"quantity": 1
}
]
}' \
https://your-store.yns.store/api/v1/carts

A 400 response with details is returned when the selections are invalid (wrong group count, disallowed duplicates, etc.).

Re-adding a bundle: If the same bundle is already in the cart with identical selections, the line quantity increments by one (capped by constituent stock). If the same bundle is already in the cart with different selections, the API returns 409 Conflict with a bundle_conflict code – remove the existing bundle from the cart first to change its options.

{
"error": "Bundle already in cart with a different configuration",
"details": ["This bundle is already in the cart with a different configuration. Remove it from the cart to change its options."]
}

Stock Overflow (409)

When the requested quantity exceeds the variant's available stock, the API returns 409 Conflict:

{
"error": "Insufficient stock",
"available": 5,
"requested": 10
}

Available stock accounts for units reserved by other active carts when stock hold is enabled.

Draft products (409)

A variant whose product is still a draft cannot be added, whatever route its id arrived by — a draft is not on sale. published and hidden both stay addable: hidden means "not listed", not "not for sale", which is what direct and QR links depend on.

{
"error": "[POST /api/v1/carts] ApiError: This product isn't available right now.",
"hint": "The product is a draft, so it cannot be purchased."
}

Subscription items

Pass subscriptionPlanId to buy a line on a recurring plan. The plan must be one of the plans listed on that product — read them from subscriptionPlanProducts[].subscriptionPlan on the product — or the add is rejected with 400.

The product's subscriptionMode decides whether a plan is optional:

subscriptionModePlan-less add
optionalAllowed — an ordinary one-time purchase
onlyRefused with 409
rollingRefused with 409

Refusals carry a machine-readable code at the start of the hint, so a client can recover without parsing prose:

CodeStatusMeaningRecovery
subscription_plan_required409The product can only be bought as a subscriptionRe-send with one of the product's plan ids
subscription_cycle_unavailable409A rolling programme has no published cycle covering today, so there is no box to shipRetry once the next box is announced — read subscriptionCycles.upcoming on the product
rolling_quantity_fixed409A rolling line holds exactly one box; the add would leave it above 1 (a second add of a line already in the cart counts)Remove the line instead of adding to it. Removals are never refused.

A cart may hold several subscriptions next to as many one-time lines as it likes, and the shopper pays for everything in one checkout. Subscriptions on the same schedule (same cadence and interval) renew together as one delivery with one shipping charge; each other schedule renews as its own delivery. Re-adding the same (variant, plan) pair just increments that line (subject to rolling_quantity_fixed for rolling programmes).

{
"error": "[POST /api/v1/carts] ApiError: This product is subscription-only",
"hint": "subscription_plan_required: pass one of the product's subscription plan ids as `subscriptionPlanId`."
}

Response

Returns the complete cart with all line items and its totals:

{
"id": "0191abc0-1234-7def-8000-000000000001",
"lineItems": [
{
"variantId": "0191abc0-0000-7000-8000-000000000100",
"quantity": 1,
"price": "2500",
"product": {
"name": "Classic Tee",
"slug": "classic-tee"
}
}
],
"subtotal": 3075,
"subtotalNet": 2500,
"subtotalGross": 3075,
"totalTax": 575,
"taxBreakdown": {
"0191abc0-1234-7def-8000-000000000001": {
"taxRate": {
"id": "0191abc0-1234-7def-8000-000000000001",
"name": "Standard VAT",
"rate": "23000"
},
"ratePercent": 23,
"tax": 575
}
}
}

Get Cart

GET /api/v1/carts/:id

Returns the full cart with all line items, product details, and computed totals.

A cart is read before the shopper has paid, so each line item's productVariant arrives with the merchant-private fields blanked — cost, digital, externalId, supplierAliases, reservedQuantity and unavailableQuantity. See Merchant-private fields.


Cart totals

Carts are priced before checkout, so the tax fields are populated as soon as the cart has line items — they are no longer null until an order is created. POST /api/v1/carts, GET /api/v1/carts/:id, and both coupon endpoints return the same shape.

FieldTypeDescription
subtotalNetnumberLine items excluding tax, in minor units
subtotalGrossnumberLine items including tax, in minor units
totalTaxnumberTax across line items and shipping (once selected), in minor units
taxBreakdownobjectTax per tax-rate id: { "<taxRateId>": { taxRate, ratePercent, tax } }. ratePercent is the percentage number (23 = 23%) — use it for display. taxRate is the raw tax-rate row, whose own rate is the internal representation (percent × 1000, e.g. "23000"). tax is in minor units.
subtotalnumberFollows the store's tax behavior — gross when inclusive, net when exclusive
shippingGrossnumber | nullShipping including tax — populated once a shipping method is selected
totalNetnumber | nullItems plus shipping, excluding tax — populated once a shipping method is selected
totalGrossnumber | nullItems plus shipping, including tax — populated once a shipping method is selected
totalnumber | nullThe amount charged — populated once a shipping method is selected

The store's tax behavior comes from taxBehavior on GET /api/v1/me and GET /api/v1/settings. Line items' productVariant carries the same net/gross price twins as products — see Prices and tax.

Stripe Tax: when the Stripe Tax module is enabled, tax is computed by Stripe at checkout, so subtotalNet, subtotalGross, totalTax, and taxBreakdown stay null on the cart. Read the tax from the checkout session or the resulting order instead.


Delete Cart

DELETE /api/v1/carts/:id

Permanently deletes a cart and all its line items.


Checkout Redirect

GET /api/v1/carts/:id/checkout

Redirects (HTTP 308) to the store's hosted checkout page for the given cart. Useful for headless integrations that need to hand off to the YNS checkout flow.

308 → https://your-store.yns.store/checkout/r/{cartId}

Apply Coupon to Cart

POST /api/v1/carts/:id/coupon

Applies a discount code to a cart. The coupon is validated against its date range, usage cap, product count requirements, minimum order value, and product/variant/collection/category/brand scope before being applied.

Request Body

FieldTypeRequiredDescription
codestringYesCoupon code to apply (matched case-insensitively, max 64 chars)
curl -X POST \
-H "Authorization: Bearer your_api_key" \
-H "Content-Type: application/json" \
-d '{"code": "WELCOME10"}' \
https://your-store.yns.store/api/v1/carts/0191abc0-1234-7def-8000-000000000001/coupon

Response (200)

Returns the full updated cart object with the coupon applied.

Validation Error (422)

When the coupon cannot be applied, a 422 is returned with a machine-readable reason:

{
"error": "Coupon cannot be applied",
"reason": "couponHasExpired"
}
ReasonDescription
couponNotFoundNo coupon with this code exists
couponIsNotValidYetThe coupon's start date is in the future
couponHasExpiredThe coupon's end date has passed
couponMaxUsesReachedThe coupon has been redeemed the maximum number of times
couponMinProductCountNotMetThe cart has fewer products than the coupon requires
couponMaxProductCountExceededThe cart has more products than the coupon allows
couponMinSubtotalNotMetThe cart's products are worth less than the coupon's minimum order value. Products count with tax and after promotions, before a plan discount or the coupon
couponIsNotApplicableNo cart line takes one of the coupon's rules. A line takes none when it matches neither the scope nor an extra rate, when it is on sale and the coupon has excludeSaleItems: true, or when it is a subscription and the coupon has appliesOnSubscription: false

For couponMinSubtotalNotMet, the body also has min, the minimum as a string of minor units in the cart currency. It is the coupon's minimum for that currency from currencyMinSubtotals, or minSubtotal as is when the currency has none:

{
"error": "Coupon cannot be applied",
"reason": "couponMinSubtotalNotMet",
"min": "20000"
}

Remove Coupon from Cart

DELETE /api/v1/carts/:id/coupon

Removes the applied coupon from a cart.

curl -X DELETE \
-H "Authorization: Bearer your_api_key" \
https://your-store.yns.store/api/v1/carts/0191abc0-1234-7def-8000-000000000001/coupon

Response (200)

{
"ok": true
}

Remove Line Item

DELETE /api/v1/carts/:id/line-items/:variantId

Removes a specific variant from the cart. Returns the updated cart.

Query Parameters

ParameterTypeDescription
subscriptionPlanIdstringMatch subscription plan when removing
lineItemIdstringUUID of one line. Removes only that line of the variant; must be a UUID or the request returns 400

For subscription items, include the subscriptionPlanId to identify the correct line item. A cart can hold several lines of one variant when they carry different properties: without lineItemId, every matching line of the variant is removed, so pass lineItemId to remove just one.