Skip to content

cart

The shopper's cart. Reads need no authentication; writes need CSRF, which the client bootstraps on its own. merge requires a signed-in customer.

Guest carts are identified by the X-Mawjod-Cart-Token header, not by the session cookie. The client captures and replays that token for you. See Cart for the whole story.

ts
mawjod.cart.get()
mawjod.cart.quote()
mawjod.cart.addLine(input)
mawjod.cart.updateLine(lineId, quantity)
mawjod.cart.removeLine(lineId)
mawjod.cart.applyCoupon(code)
mawjod.cart.removeCoupon()
mawjod.cart.merge(guestToken?)

cart.get()

ts
get(): Promise<Cart>

GET /api/v1/cart. Reads the caller's cart with refreshed prices and availability.

ts
const cart = await mawjod.cart.get()

cart.id is null when the caller has no cart yet. The API answers a zeroed cart rather than a 404, so "no cart" is a normal state and not an error to handle.

Cart

ts
interface Cart {
  id: string | null
  status: string                  // 'active'
  is_guest: boolean
  guest_token: string | null      // non-null exactly once, at creation
  item_count: number
  subtotal: Money
  has_unpurchasable_lines: boolean
  lines: CartLine[]
  adjustments: CartAdjustment[]
  last_activity_at: string | null
}

interface CartLine {
  id: string
  variant_id: string
  sku: string
  name_ar: string
  name_en: string
  option_selection: Record<string, unknown>  // reserved; always {} in release one
  quantity: number
  unit_price: Money
  line_total: Money
  purchasable: boolean
}

Lines carry both locales, unlike the catalog which resolves one server-side. A stored line therefore renders correctly whichever locale the shopper was in when they added it.

subtotal is the plain line total. Discounts and tax do not appear on Cart; they live on cart.quote().

cart.quote()

ts
quote(): Promise<CartQuote>

GET /api/v1/cart/quote. Reprices the cart and re-evaluates promotions. It does not mutate anything.

ts
interface CartQuote {
  subtotal: Money
  discount_total: Money
  discounted_subtotal: Money
  tax_amount: Money
  tax_rate_basis_points: number     // 1400 = 14%
  applied_discount: AppliedDiscount | null
  lines: QuotedLine[]               // [{ line_id, gross, discount, net }]
  rejected_discounts: RejectedDiscount[]
}

interface AppliedDiscount {
  discount_id: string
  name_ar: string
  name_en: string
  type: string
  scope: string
  coupon_code: string | null
  amount: Money
}

discounted_subtotal.minor is what checkout's expected_items_subtotal_minor guard wants, and what a fulfillment quote's subtotal_minor wants.

rejected_discounts lists promotions that were evaluated and did not apply, each with a reason.

cart.addLine()

ts
addLine(input: AddCartLineInput): Promise<Cart>

POST /api/v1/cart/lines. Creates the cart when there is none.

ts
interface AddCartLineInput {
  variant_id: string
  quantity: number                      // 1-999
  options?: Record<string, never>       // reserved; release one accepts only {}
}

Adding a variant that is already in the cart sums into the existing line rather than creating a second one.

For a guest, this is the response that issues guest_token. The client stores it and sends it as X-Mawjod-Cart-Token from here on. It is never issued again.

A variant that cannot be sold is 409 variant_not_purchasable.

cart.updateLine()

ts
updateLine(lineId: string, quantity: number): Promise<Cart>

PUT /api/v1/cart/lines/{lineId}. Replaces the line's quantity (1 to 999). This is a replace, not a delta.

cart.removeLine()

ts
removeLine(lineId: string): Promise<Cart>

DELETE /api/v1/cart/lines/{lineId}. Returns the cart without that line.

cart.applyCoupon()

ts
applyCoupon(code: string): Promise<CartQuote>

POST /api/v1/cart/coupon. The code is 3 to 40 characters and matched case-insensitively.

Returns a CartQuote, not a Cart.

Every refusal is one code, 409 pricing_conflict, covering unknown, inactive, expired, unqualified, fully redeemed, and beaten-by-a-better-promotion. The problem document may carry a reason slug ('expired', for instance), readable through error.problem.reason.

cart.removeCoupon()

ts
removeCoupon(): Promise<CartQuote>

DELETE /api/v1/cart/coupon. The returned quote has applied_discount: null.

cart.merge()

ts
merge(guestToken?: string): Promise<Cart>

POST /api/v1/cart/merge. Requires an authenticated customer.

Defaults to the stored guest token, so you usually call it with no arguments. It throws a plain Error when no token was passed and none is stored: there is nothing to merge, which is a programming mistake rather than an API failure.

ts
const cart = await mawjod.cart.merge()

cart.is_guest    // false
cart.adjustments // [{ reason: 'merged_quantity_summed', line_id, variant_id,
                 //    previous_quantity, quantity }]

The merge is additive and deterministic: identical lines sum, guest-only lines move across, and replaying the same token merges only once. On success the client clears the stored token.

The token moves to the body here

Every other cart call carries the guest token in the X-Mawjod-Cart-Token header. This one takes it in the request body as guest_token. The client handles the difference; it is documented because it is visible in the network tab.

checkout.place can also merge inline, by taking guest_token in its own body.

Errors

CodeStatusWhere
variant_not_purchasable409addLine
pricing_conflict409applyCoupon
validation_failed422everywhere
rate_limited429everywhere
store_unavailable503everywhere

merge also requires authentication, so 401 unauthenticated applies to it.

In Nuxt

ts
const { cart, lines, itemCount, subtotal, latestQuote, addLine, updateLine, removeLine } = useCart()

useCart() keeps the cart in shared state and writes every mutation's response into it. See Composables → useCart.