Cart preview & price locks
Scent orders are always created from the caller’s own cart — order lines are never part of the create request itself. The flow is: build a cart, preview it, optionally lock the preview’s prices, then create the order.
1. Build the cart
Section titled “1. Build the cart”POST /api/v1/scent/cart/items{ "variantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": 2 }Pass ?customerRef= on every cart call if you’re tracking separate in-progress carts per one of
your own end customers (max 128 characters); omit it for a single, bare partner-level cart.
2. Preview before you ever create an order
Section titled “2. Preview before you ever create an order”GET /api/v1/scent/cart/checkout-preview{ "state": "SUCCESS", "payload": { "cartId": "...", "lines": [ { "variantId": "...", "quantity": 2, "ok": true, "issue": null, "listPrice": 249.0, "...": "..." } ], "canCheckout": true, "cartIssue": null, "pricedAt": "2026-08-02T10:00:00Z", "lockId": null, "lockExpiresAt": null, "chargedWithVat": 498.0 }}Always call this before POST /api/v1/orders/scent. It’s the only way to find out, ahead of
time, whether every line in the cart can actually be checked out. canCheckout is true only
when every line reports ok: true, the cart is non-empty, and there’s no already-pending order on
it. Per-line issue is one of:
| Issue | Meaning | What to do |
|---|---|---|
UNAVAILABLE |
The variant isn’t currently sellable — out of stock everywhere, or deactivated since it was added to the cart. | Don’t offer this line to the customer as-is. Remove it or swap it for an in-stock alternative; retrying the same request won’t change the outcome. |
INSUFFICIENT_STOCK |
Some stock exists, but less than the requested quantity. | Show the customer the real ceiling — the line’s availableQuantity is populated when availability is Limited — and let them lower the quantity rather than resubmitting the same one. |
PRICE_BELOW_FLOOR |
The line’s margin would fall below the platform’s required minimum — a vendor cost or price moved since it was last priced. | This isn’t customer-fixable. Drop the line (or treat it as temporarily unavailable) and don’t keep retrying the same price/quantity; re-preview later if you want to try again. |
NOT_PRICED |
No active price exists for this variant/partner combination. | Treat it the same as UNAVAILABLE for checkout purposes — don’t offer it until pricing is configured on your account. |
Two cart-level issues (cartIssue, not per-line) can also block checkout:
CART_HAS_PENDING_ORDER— an order already exists against this exact cart and hasn’t reached a terminal state yet. The blocking order’s id is echoed back aspendingOrderReference; poll or subscribe to webhooks for that order and try checkout again once it reaches a terminal status (Completed/Failed/Cancelled) — don’t just retry the checkout itself, it will keep rejecting until the pending order resolves.PRODUCT_ACCESS_DISABLED— Linra has paused this partner’s ability to start anything new on Scent (an internal hold applied at the partner level, independent of any one cart or line). It can appear even on an otherwise-empty cart, and it takes precedence overCART_HAS_PENDING_ORDERwhen both would otherwise apply — it’s the more fundamental “nothing new right now” condition. Orders already in flight are unaffected and keep fulfilling normally. There’s nothing to retry around: surface a generic “temporarily unavailable” message to the customer and stop attempting new checkouts until the hold is lifted — contact your Linra account manager if you weren’t expecting it. The same condition surfaces as422 BUSINESS_PRODUCT_ACCESS_DISABLEDif you skip the preview and go straight to adding items or creating an order while paused.
3. (Optional) Lock the previewed prices
Section titled “3. (Optional) Lock the previewed prices”Prices can move between when you preview and when you actually create the order — a vendor cost change, a margin-floor recalculation, a promotional price update. If your checkout has any delay between “show the customer a price” and “charge them” (a payment step, a confirmation screen), lock the preview:
GET /api/v1/scent/cart/checkout-preview?lock=trueThis only succeeds in taking a lock when the preview itself reports canCheckout: true — a broken
basket is never locked. The response adds:
{ "lockId": "b8f1c2a0-...", "lockExpiresAt": "2026-08-02T10:15:00Z"}The lock is valid for 15 minutes from the moment it’s taken. Any cart mutation (add, change quantity, remove, clear) invalidates the active lock immediately — a lock always reflects the exact cart contents at the moment it was taken, never a moving target. Only one lock can be active per cart at a time; taking a new one supersedes the old.
4. Create the order
Section titled “4. Create the order”POST /api/v1/orders/scent{ "partnerId": "...", "externalReference": "po-2026-04-1029", "geoScopeId": "...", "sagaId": "...", "shippingAddress": { "recipient": "...", "line1": "...", "city": "...", "nationalAddress": "RAKB2837", "country": "SA", "phone": "..." }, "lockId": "b8f1c2a0-..."}If you omit lockId, the order is priced against the CURRENT live prices at the moment of
creation (re-running the exact same checks the preview ran). If you pass a lockId, the order is
charged exactly the locked per-line prices — but the margin-floor check still re-runs against
those locked figures, so a lock does not bypass the floor; it only protects you from routine price
drift, not from a floor violation.
A lockId that’s unknown, doesn’t belong to your own cart, has expired, or no longer matches the
cart’s current version (because the cart was mutated after the lock was taken) is rejected as
400 VALIDATION_PRICE_LOCK_INVALID or 400 VALIDATION_PRICE_LOCK_EXPIRED — both collapse to a
generic “the lock isn’t usable” signal by design, so treat either the same way: re-preview
(optionally re-lock) and retry.
Shipping fee
Section titled “Shipping fee”chargedWithVat on the cart response and the checkout preview is the goods total plus an
order-level shipping fee — the same fee is stamped onto the order at create time, so cart, preview,
and the created order can never disagree. The preview response carries the fee’s own breakdown
alongside the total:
{ "chargedWithVat": 528.0, // goods + shipping fee, VAT-inclusive — what checkout would charge "shippingFeeAmount": 30.0, // net (ex-VAT) fee "shippingFeeVatAmount": 4.5, // standard-rated VAT on the fee "shippingFeeAmountWithVat": 34.5, "freeShippingThreshold": 499.0, // SAR — the goods total (incl. VAT, before the fee) that waives the fee "isFreeShipping": false}- The fee is a flat SAR 30 (net, ex-VAT) today — standard-rate VAT is added on top to get
shippingFeeAmountWithVat. It’s waived entirely (every fee field reads0,isFreeShipping: true) once the cart’s goods total — including VAT, before any discount, and before the fee itself — reaches the SAR 499 free-shipping threshold. Both values are ops-configurable and may change; read the fields, don’t hard-code them. - The delivery zone the fee is computed against is the shipping address’s
city(checked against a “within Riyadh” allowlist) — see Integration journey, step 6 for thenationalAddressfield the address now requires as the basis for that zone determination. - The fee is never refunded by the returns path. A full goods return still reverses every goods/commission leg, but the order’s shipping-fee charge (and its VAT) stands — factor this into any refund-amount expectations your integration builds.
- A locked preview (
lock=true) freezes the fee along with the goods price — the order created from that lock is charged exactly the frozen fee, even if the free-shipping threshold or the flat fee amount changes before the lock is redeemed. A lock taken before this feature existed (if you’re somehow still holding one) redeems at a zero fee rather than an error — its quote genuinely never showed one.
Common mistake to avoid
Section titled “Common mistake to avoid”Don’t skip the preview call “to save a round trip.” Order-create will still enforce every check
the preview reports, so skipping it doesn’t make checkout faster — it only means you find out about
a PRICE_BELOW_FLOOR or INSUFFICIENT_STOCK issue from a failed order-create instead of a cheap
read, and you lose the ability to show the customer accurate pricing before you charge them.

