Skip to content

checkout

Turns a cart into an order. Requires an authenticated, verified customer. There is no guest checkout in release one.

Checkout never takes money. Even a card order is placed unpaid; the payment session starts afterwards with orders.pay.

See Checkout for the full flow, retry rules and failure handling.

ts
mawjod.checkout.place(input, options?)

checkout.place()

ts
place(input: CheckoutInput, options?: CheckoutOptions): Promise<CheckoutResult>

POST /api/v1/customer/checkout.

ts
const { order, idempotencyKey, operationId } = await mawjod.checkout.place({
  fulfillment_method: 'delivery',
  payment_method: 'cod',
  address_id: address.id,
  expected_items_subtotal_minor: quote.discounted_subtotal.minor,
})

CheckoutInput

ts
interface CheckoutInput {
  fulfillment_method: 'delivery' | 'pickup'
  payment_method: string
  address_id?: string | null
  pickup_location_id?: string | null
  guest_token?: string | null
  expected_items_subtotal_minor?: number | null
  operation_id?: string
}
FieldNotes
fulfillment_methoddelivery or pickup
payment_methodNot an enum. See below.
address_idRequired for delivery
pickup_location_idRequired for pickup
guest_token64 characters. Merges an unmerged guest cart inline.
expected_items_subtotal_minorOptimistic-concurrency guard. A mismatch is a 409.
operation_idUUIDv7 identifying this attempt; generated when omitted

payment_method is not an enum

cod is always offered. paymob only exists when the deployment has the gateway configured. A future method needs no SDK release, which is why the type is PaymentMethod = 'cod' | 'paymob' | (string & {}).

Read the offered set at runtime, from store.settings()checkout.allowed_payment_methods, or from a fulfillment quote's allowed_payment_methods, which is narrowed to the chosen zone or pickup point. Prefer the quote when you have one.

Choosing paymob places the order unpaid. Read order.payment.requires_action afterwards; do not infer it from the method.

CheckoutOptions

ts
interface CheckoutOptions {
  idempotencyKey?: string
}

Sent as the Idempotency-Key header. 16 to 128 printable ASCII characters; a UUIDv7 is a fine choice and is what the client generates when you omit it.

Never send idempotency_key in the body

Scribe's generated docs show a body field by that name. It is not an input. The server overwrites it from the header before validating, and the client deletes it from the body if it finds one. The header is the only authority.

CheckoutResult

ts
interface CheckoutResult {
  order: Order
  idempotencyKey: string
  operationId: string
}

Both identifiers come back so a retry can reuse them. Keep them for as long as the attempt might be retried.

The idempotency pair

Two values travel together and must both be reused for a replay:

ValueWhereWhat
Idempotency-Keyheader16 to 128 printable ASCII characters
operation_idbodyUUIDv7; also the stock-reservation identity

The server hashes the key against exactly these fields:

operation_id
fulfillment_method
payment_method
address_id
pickup_location_id
expected_items_subtotal_minor

Same key, same values → replay of the original response. Same key, any one of those changed → conflict, not a replay.

Retry semantics

Transport failure: the request never got an answer, or the answer never got back. The order may or may not exist. Retry with the same input and the same pair; the server replays if it already placed the order.

Stale-cart failure: cart_price_changed, cart_not_purchasable or insufficient_stock. Do not retry. Refetch the cart, show what changed, ask the shopper to confirm, then call place() again with a fresh pair. Reusing the key with a corrected expected_items_subtotal_minor is answered as a conflict, not a replay, and silently retrying would charge someone a price they never agreed to.

Failure families

CodeStatusMeaningResponse
cart_price_changed409Prices movedRefetch, show, fresh place()
cart_not_purchasable409A line can no longer be soldRefetch, show, fresh place()
insufficient_stock409Not enough stockRefetch, show, fresh place()
cart_empty422Nothing to orderSend them to the catalogue
cart_not_found422No cart for this callerSend them to the catalogue
payment_method_unavailable422Method not offered hereRe-read the allowed set
customer_not_verified403Identity not verified, where the store requires itVerification screen, not the cart

409 means refetch. 422 means rewrite the request. 403 means route to verification.

The 403 only exists on a store that has turned on auth.customer_verification_required, which is off by default. See store.settings() → Verification.

ts
import { isCheckoutError, isStaleCartError } from '@mawjod/api'

isStaleCartError(error)  // the three 409s
isCheckoutError(error)   // all seven

error.detail never contains quantities, prices or addresses. Do not parse it. Compute the diff from the refetched cart.

401 unauthenticated, 422 validation_failed, 429 rate_limited and 503 store_unavailable apply as everywhere else.

The returned order

place() runs the payload integrity guard: an order with lines: [] throws PayloadIntegrityError rather than resolving, because an order is created from at least one cart line and an empty one is a lost payload arriving as a valid 201. See Errors → the integrity guard.

The Order shape is documented on orders.

In Nuxt

ts
const { place, retry, order, attempt, staleCart, isStale, pending, reset } = useCheckout()

useCheckout() mints the pair before the call and keeps it on attempt, because an attempt that fails is exactly the one that needs retrying under the same key. retry() replays the stored input under the stored pair, which is the transport-failure path only. See Composables → useCheckout.