SazitoSazito SDKv1.4.0
Guides

Checkout

Drop-in multi-step checkout UI for Next.js apps built on the Sazito SDK.

Overview

@sazito/checkout is a ready-made, fully typed checkout flow with built-in state management. It handles:

  • Cart review (edit quantities, remove items, apply discount codes)
  • Shipping address form with region / city cascading selects
  • Shipping method selection (multiple groups supported)
  • Payment method selection and gateway redirect
  • Post-payment result screen (success, pending, failed, stock violated) with order code, public ID, shipping methods, purchased-item details, and totals
  • Automatic pending-payment polling after gateway return

Steps run in order: cart → shipping → payment → result

The payment step is the last interactive step: its CTA (انجام پرداخت / "Pay now") finalizes the order directly — there is no separate review step. Payment methods show the default method first, online methods next, and non-default pay-on-delivery/card-to-card choices last. The order-details action on a successful result opens in a new tab.

Installation

pnpm add @sazito/client-sdk @sazito/checkout react@18 react-dom@18

The checkout package requires Node.js 18 or newer. It ships ESM and CommonJS builds, bundled TypeScript declarations, and server-safe entry points:

Entry pointUse case
@sazito/checkout/coreFramework-independent engine, state, actions, selectors, events, and formatters.
@sazito/checkout/reactCheckoutProvider, SazitoProvider, and checkout hooks.
@sazito/checkout/nextDrop-in checkout page, rendered UI, hooks, and customizable primitives.
@sazito/checkout/next/serverNext.js-compatible GET/POST gateway callback handlers.
@sazito/checkout/serverFramework-neutral Web Request/Response callback handlers.
@sazito/checkout/styles.cssSelf-contained RTL/LTR styles and theme variables.

Import the stylesheet once — typically in your root layout:

import '@sazito/checkout/styles.css';

Next.js setup

1. Add environment variables

NEXT_PUBLIC_SAZITO_DOMAIN=shop.example.com
SAZITO_STORE_DOMAIN=shop.example.com

2. Add the SDK provider

// app/providers.tsx
'use client';

import { createSazitoClient } from '@sazito/client-sdk';
import { SazitoProvider } from '@sazito/checkout/next';

const client = createSazitoClient({
  domain: process.env.NEXT_PUBLIC_SAZITO_DOMAIN!,
});

export function Providers({ children }: { children: React.ReactNode }) {
  return <SazitoProvider client={client}>{children}</SazitoProvider>;
}

Import the styles and provider once:

// app/layout.tsx
import '@sazito/checkout/styles.css';
import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="fa" dir="rtl">
      <body><Providers>{children}</Providers></body>
    </html>
  );
}

3. Add the checkout page

// app/checkout/page.tsx
'use client';

import { SazitoCheckoutPage } from '@sazito/checkout/next';

export default function CheckoutPage() {
  return (
    <SazitoCheckoutPage
      credentials={{ cart: { identifier: '<cart-identifier>' } }}
      config={{ locale: 'fa', continueShoppingUrl: '/' }}
    />
  );
}

Replace <cart-identifier> with the identifier created through client.cart.

4. Add the payment callback route

// app/checkout/[...callback]/route.ts
import { SazitoCheckout } from '@sazito/checkout/next/server';

export const { GET, POST } = SazitoCheckout({
  domain: process.env.SAZITO_STORE_DOMAIN!,
  checkoutPath: '/checkout',
}).handlers;

That is all the host needs for gateway returns. Keep both GET and POST: different gateways use different callback methods. The SDK validates the callback, verifies the payment on the server, redirects back to /checkout, and shows the final result. Use a Route Handler here, not a Server Action.

5. Test the flow

  • Open /checkout and complete a test payment.
  • Confirm the gateway return reaches the catch-all route.
  • Confirm the browser returns to /checkout and displays the payment result.

Props reference

SazitoProvider

Wraps the app once and exposes the existing SDK client to every checkout below it. Create the client with createSazitoClient; the provider does not create a client from a domain.

PropTypeDescription
clientSazitoClientRequired pre-built SDK client. Checkout reuses this client's credentials and configuration.
childrenReactNodeApplication or checkout subtree.

SazitoCheckoutPage

PropTypeDescription
credentialsCheckoutCredentialsCart (and optionally invoice) identifiers to attach to.
configCheckoutConfigLocale, theme, URLs, event hook, behaviour flags.
paymentReturnCheckoutPaymentReturnOptional callback data for custom integrations. The server handler setup does not require this prop.
paymentReturnParamsRecord<string, string>Legacy query-only callback parameters. The server handler setup does not require this prop.
classNamestringExtra class on the root element.
getOrderDetailsUrl(order: CheckoutOrder) => string | nullBuild the result screen's order-details link using your theme's routes and host. Return null to hide it.
renderNextButton(props: RenderButtonProps) => ReactNodeReplace the continue / next button across all steps.
renderBackButton(props: RenderButtonProps) => ReactNodeReplace the back button across all steps.
emptyCartEmptyCartOptionsCustomize the built-in empty state's title, description, icon, action label, or action URL.
renderEmptyCart(props: RenderEmptyCartProps) => ReactNodeReplace the empty-cart screen. Receives the resolved content and continueShoppingUrl.
renderResult(props: RenderResultProps) => ReactNodeReplace the post-payment result screen. Receives the result, status, finalized order, continue-shopping URL, and retry action.

CheckoutCredentials

interface CheckoutCredentials {
  cart?: { id?: number; identifier: string };
  invoice?: { id: number; identifier: string };
  payment?: { id: number; identifier: string };
}

Typically only cart.identifier is needed. Supply invoice to re-attach to an already-created invoice. Payment credentials are restored automatically after the server callback handler redirects to checkout.

CheckoutConfig

FieldTypeDefaultDescription
locale'fa' | 'en''fa'UI language. Controls all labels, RTL/LTR layout, and number formatting.
direction'rtl' | 'ltr'derived from localeOverride text direction explicitly.
themeCheckoutTheme—Visual overrides — see Theming.
continueShoppingUrlstring—URL for the back button on the cart step and the "continue shopping" link on the result screen.
returnUrlstring—Reserved for gateways that support a caller-provided return URL. Legacy Sazito callbacks use their generated nested result path.
pollIntervalMsnumber15000How often (ms) to poll for a pending payment result after gateway return.
debugbooleanfalseLog detailed, correlated payment-verification parsing and checkout state transitions. Sensitive values are redacted.
currencyLabelstringlocale defaultOverride the currency unit label (e.g. 'تومان').
onEvent(event: CheckoutEvent) => void—Analytics hook — called for every checkout lifecycle event. See Analytics.

Theming

All visual tokens are CSS custom properties. Pass a theme object to override them:

config={{
  theme: {
    accent: '#7c3aed',           // primary colour — buttons, active states, selected rows
    accentForeground: '#ffffff', // text rendered on top of accent
    accentSoft: '#ede9fe',       // subtle accent tint — hover, selection background
    background: '#ffffff',       // page background
    foreground: '#0f172a',       // primary text
    muted: '#f1f5f9',            // muted surface background
    mutedForeground: '#64748b',  // secondary text
    border: '#e2e8f0',           // borders and dividers
    card: '#ffffff',             // form, product card, shipping card background
    summaryBackground: '#f8fafc', // order-summary sidebar background
    danger: '#ef4444',           // error and destructive states
    success: '#22c55e',          // success and completed states
    successForeground: '#ffffff', // content on solid success surfaces
    logoBackground: '#ffffff',   // plate behind shipping-provider logos
    shippingNeutral: '#f1f5f9', // fallback surface for white shipping colors
    shippingNeutralForeground: '#475569', // content on that neutral surface
    radius: 12,                  // base border-radius in px (scaled per element)
    fontFamily: 'inherit',       // font — defaults to host page font
  },
}}

Reuse your application's design tokens

Theme colors and fontFamily accept any valid CSS value, including references to variables already defined by the host application. This is the recommended way to connect checkout to an existing design system:

<SazitoCheckoutPage
  credentials={{ cart: { identifier: '<cart-identifier>' } }}
  config={{
    locale: 'fa',
    theme: {
      accent: 'var(--color-primary)',
      accentForeground: 'var(--color-on-primary)',
      accentSoft: 'var(--color-primary-soft)',
      background: 'var(--color-background)',
      foreground: 'var(--color-foreground)',
      muted: 'var(--color-muted)',
      mutedForeground: 'var(--color-muted-foreground)',
      border: 'var(--color-border)',
      card: 'var(--color-card)',
      summaryBackground: 'var(--color-surface)',
      danger: 'var(--color-danger)',
      success: 'var(--color-success)',
      successForeground: 'var(--color-on-success)',
      logoBackground: 'var(--color-logo-plate)',
      shippingNeutral: 'var(--color-muted)',
      shippingNeutralForeground: 'var(--color-muted-foreground)',
      fontFamily: 'var(--font-sans)',
      radius: 12,
    },
  }}
/>

CSS fallbacks work as usual—for example, accent: 'var(--color-primary, #4f46e5)'. The host variables must be defined on :root or another ancestor of the checkout. When a light/dark theme changes their values, checkout updates automatically without a React rerender:

:root {
  --color-primary: #6d28d9;
  --color-on-primary: #ffffff;
  --color-background: #ffffff;
  --color-foreground: #18181b;
  --color-card: #ffffff;
  --color-border: #e4e4e7;
}

[data-theme='dark'] {
  --color-background: #09090b;
  --color-foreground: #fafafa;
  --color-card: #111113;
  --color-border: #27272a;
}

CSS-only token mapping

For a stylesheet-driven integration, add a class to the checkout and map its native variables. Import these overrides after @sazito/checkout/styles.css:

<SazitoCheckoutPage
  className="store-checkout"
  credentials={{ cart: { identifier: '<cart-identifier>' } }}
/>
.szc-root.store-checkout {
  --szc-accent: var(--color-primary);
  --szc-accent-foreground: var(--color-on-primary);
  --szc-bg: var(--color-background);
  --szc-fg: var(--color-foreground);
  --szc-muted: var(--color-muted);
  --szc-muted-fg: var(--color-muted-foreground);
  --szc-border: var(--color-border);
  --szc-card: var(--color-card);
  --szc-summary-bg: var(--color-surface);
  --szc-danger: var(--color-danger);
  --szc-success: var(--color-success);
  --szc-font: var(--font-sans);
  --szc-radius: var(--radius-lg);
}

Do not set the same token through both approaches. Values supplied through config.theme are written as inline --szc-* properties and take precedence over ordinary stylesheet declarations. The CSS-only approach is also required when radius must reference a CSS variable, because config.theme.radius accepts a number in pixels.

Payment gateway return

Gateway callbacks use a nested URL such as:

/checkout/paymentinplaceresult/payment/304/identifier/6f8a717eb211f0839e522a5358932968

The catch-all Route Handler from the setup section receives this URL. It parses the GET or POST fields and verifies the payment. When the API returns show_order, it carries that confirmed result to checkoutPath using a small HTML response and tab-scoped storage. Checkout displays the original success without calling process_payment_step again. If storage is unavailable, the HTML response itself displays order confirmation. Other responses use a 303 redirect and a status read; pending results start polling automatically.

Optional server settings:

FieldDefaultDescription
checkoutPath/checkoutCheckout result page. Locale-prefixed paths such as /fa/checkout are supported.
maxCallbackBodyBytes65536Maximum accepted callback body size.
gatewayResultMarkersBuilt-in Sazito listOverride the accepted callback route names for a custom gateway.
debugfalseLog server-side callback errors without exposing them to the browser.

The response uses Cache-Control: no-store and Referrer-Policy: no-referrer. Malformed callbacks are rejected before the payment API is called. After the return is recognized, checkout removes the temporary payment parameters from the browser URL.

Test the return flow with mock data

The documentation checkout includes local payment-result scenarios, so you can exercise verification without creating an order or contacting a payment gateway. Start the docs app and open the demo:

pnpm --dir docs dev
http://localhost:3000/checkout-demo?mockPayment=success-minimal

You can also change Payment result in the demo toolbar:

ScenarioExpected result
success-minimalSuccess with only the required order identifiers. This protects against the former Invalid payment step result failure when totals are absent.
success-fullSanitized real payment-in-place show_order response with product, shipping, totals, receipt, and nullable fields.
pending-successPending callback followed by a successful JSON status poll.
failureGateway-declared payment failure.
malformedIncomplete show_order payload; this should remain a compact success instead of displaying a false failure.
liveNormal checkout using the configured domain and cart identifier.

Mock mode simulates payment verification and checkout finalization. It also asserts that the initial callback is sent as application/x-www-form-urlencoded with payment_identifier and payload[RefId]; the pending poll must remain JSON. No real payment, cart, or gateway redirect is performed.

Analytics

Pass onEvent to receive structured events at every step. Use it to send to any analytics provider:

config={{
  onEvent(event) {
    analytics.track(event.name, {
      step: event.step,
      value: event.value,         // order total in Rials where applicable
      ...event.metadata,
      timestamp: event.timestamp,
    });
  },
}}

Event names

EventFired when
checkout_viewedCheckout mounts
step_viewedUser lands on a step
address_submittedShipping address saved
shipping_rate_selectedUser picks a shipping method
discount_appliedDiscount code accepted
discount_removedDiscount code removed
payment_method_selectedUser picks a payment gateway
payment_initiatedOrder placed, gateway redirect started
payment_succeededGateway return resolved as success
payment_failedGateway return resolved as failed
payment_pendingGateway return resolved as pending (polling starts)

Validation gates

The next button disables automatically — no configuration needed:

StepDisabled when
CartCart is empty
ShippingShipping rates loaded but no rate selected (skipped for digital-only orders)
PaymentNo payment methods available, or none selected

Replacing the empty-cart screen

For copy and icon changes, keep the responsive built-in layout and pass emptyCart:

<SazitoCheckoutPage
  config={{ locale: 'fa', continueShoppingUrl: '/products' }}
  emptyCart={{
    title: 'هنوز چیزی انتخاب نکرده‌اید',
    description: 'محصولات فروشگاه را ببینید و انتخاب‌های خود را به سبد اضافه کنید.',
    actionLabel: 'مشاهده محصولات',
    icon: <MyBagIcon />,
  }}
/>

Use renderEmptyCart when the storefront has its own empty-state component:

<SazitoCheckoutPage
  credentials={{ cart: { identifier: '<cart-identifier>' } }}
  config={{ locale: 'fa', continueShoppingUrl: '/products' }}
  renderEmptyCart={({ continueShoppingUrl, title, description }) => (
    <EmptyCart
      actionHref={continueShoppingUrl ?? '/products'}
      actionLabel="Browse products"
      title={title}
      description={description}
    />
  )}
/>

Set getOrderDetailsUrl on SazitoCheckoutPage (or SazitoCheckout) to use your storefront's order detail route. The callback receives the completed order, including id and orderIdentifier. Include both in the URL so the destination can call client.orders.get(id, orderIdentifier).

'use client';

import { SazitoCheckoutPage } from '@sazito/checkout/next';

export function ThemeCheckout() {
  return (
    <SazitoCheckoutPage
      config={{ locale: 'fa', continueShoppingUrl: '/fa/products' }}
      getOrderDetailsUrl={(order) =>
        `/fa/orders/${encodeURIComponent(String(order.id))}?identifier=${encodeURIComponent(order.orderIdentifier)}`
      }
    />
  );
}

The returned URL is used as supplied: return a relative path for the current host, or an absolute URL such as https://shop.example.com/fa/orders/... for a different storefront host. Encode dynamic path and query values. Define the callback in a Client Component in Next.js, inside the existing SazitoProvider.

Without this prop, checkout keeps /orderinfo/{id}/{identifier}, using the origin of continueShoppingUrl when it is absolute. Returning null hides the order-details action without falling back to that default. The link continues to open in a new tab. No callback runs when the payment result has no order. If you supply renderResult, your custom renderer owns its links.

Replacing the result screen

Use renderResult to replace the complete post-payment screen while keeping the checkout engine's payment verification and pending polling:

import type { RenderResultProps } from '@sazito/checkout/next';

function StorefrontResult({
  status,
  order,
  result,
  continueShoppingUrl,
  onRetry,
}: RenderResultProps) {
  if (status === 'pending') return <PaymentVerification />;

  if (status !== 'success') {
    return (
      <PaymentError message={result?.message} onRetry={onRetry} />
    );
  }

  return (
    <OrderSuccess
      order={order}
      continueHref={continueShoppingUrl ?? '/products'}
    />
  );
}

<SazitoCheckoutPage
  credentials={{ cart: { identifier: '<cart-identifier>' } }}
  config={{ locale: 'fa', continueShoppingUrl: '/products' }}
  renderResult={(props) => <StorefrontResult {...props} />}
/>

onRetry returns the checkout to the payment step. order is populated when the payment response includes valid order details; a terminal show_order success may omit it while the order is still being finalized server-side.

RenderResultProps

PropTypeDescription
resultCheckoutResult | nullComplete engine result. Includes the returned message and finalized order when available. It can be null during the initial resolving render.
status'success' | 'failed' | 'pending' | 'stock_violated'Normalized status. Defaults to pending until a result is resolved.
orderCheckoutOrder | undefinedConvenience reference to result.order. Available for valid show_order and pending responses.
continueShoppingUrlstring | undefinedValue configured through config.continueShoppingUrl. The custom renderer decides whether and how to navigate to it.
onRetry() => voidClears the result view and returns to the payment step. Use for failed and stock_violated states.

Supplying renderResult replaces only the result content; the checkout root, direction, theme variables, stepper, and .szc-result-wrap layout container remain mounted. The callback runs only while the active step is result. Returning null intentionally leaves the result area empty.

The slot is also available when composing the React UI directly:

<CheckoutProvider client={sazito} credentials={credentials}>
  <SazitoCheckout renderResult={(props) => <StorefrontResult {...props} />} />
</CheckoutProvider>

Replacing action buttons

Use renderNextButton / renderBackButton to swap the built-in action buttons for components from your own design system. Both receive pre-computed checkout state. The next button renders under the order summary on desktop and inside the sticky bottom bar (next to the payable total) on mobile; the back button renders at the end of the step content.

import type { RenderButtonProps } from '@sazito/checkout/next';

<SazitoCheckoutPage
  credentials={{ cart: { identifier: '<cart-identifier>' } }}
  config={{ locale: 'fa' }}
  renderNextButton={({ loading, disabled, onClick, children }) => (
    <MyButton loading={loading} disabled={disabled} onClick={onClick}>
      {children}
    </MyButton>
  )}
  renderBackButton={({ onClick, children }) => (
    <MyGhostButton onClick={onClick}>{children}</MyGhostButton>
  )}
/>

RenderButtonProps

PropTypeDescription
loadingbooleanAction is in-flight — show a spinner
disabledbooleanValidation not met — button should be inert
onClick() => voidTriggers the checkout action for this step
childrenReactNodeLocalised label for the current step

The children label changes per step: ثبت سفارش, ثبت اطلاعات ارسال, ادامه به پرداخت, انجام پرداخت (Persian) or the equivalent English strings.

Replacing primitives with asChild

All built-in primitives accept an asChild prop. When set, the component merges its behaviour props onto your single child element instead of rendering its own DOM node — the same pattern used by Radix UI and shadcn/ui.

Class names are merged (primitive class + child class). Event handlers are composed (both called).

Button

import { Button } from '@sazito/checkout/next';

<Button asChild loading={isLoading} disabled={isDisabled} onClick={handleClick}>
  <MyButton variant="filled">Continue</MyButton>
</Button>

Props forwarded: className, disabled, aria-busy, onClick, and all standard button attributes.

Field

Keeps the label, optional marker, and error message. Swaps the <input> element.

import { Field } from '@sazito/checkout/next';

<Field label="Email" id="email" error={errors.email} asChild>
  <MyInput placeholder="you@example.com" />
</Field>

Spinner

import { Spinner } from '@sazito/checkout/next';

<Spinner asChild>
  <MyLoadingIndicator />
</Spinner>

ErrorBanner

Merges role="alert" and class onto your component.

import { ErrorBanner } from '@sazito/checkout/next';

<ErrorBanner message={errorMessage} asChild>
  <MyAlert />
</ErrorBanner>

SectionTitle

Swaps the default <h3> for any heading element or typography component.

import { SectionTitle } from '@sazito/checkout/next';

<SectionTitle asChild>
  <h2 className="text-xl font-bold">Payment method</h2>
</SectionTitle>

ProductPlaceholder

Replaces the default box icon shown when a product has no image.

import { ProductPlaceholder } from '@sazito/checkout/next';

<ProductPlaceholder asChild>
  <img src="/fallback-product.png" alt="" />
</ProductPlaceholder>

Programmatic control with useCheckout

Use useCheckout inside any component rendered within CheckoutProvider to read state or trigger actions directly.

'use client';

import { useCheckout } from '@sazito/checkout/next';

export function DiscountWidget() {
  const { state, actions } = useCheckout();

  return (
    <div>
      <input
        value={state.discountCode}
        onChange={(e) => actions.setDiscountCode(e.target.value)}
      />
      <button
        onClick={() => actions.applyDiscount()}
        disabled={state.flags.applyingDiscount}
      >
        Apply
      </button>
      {/* Invalid/expired codes set an inline error, not the global banner. */}
      {state.discountError ? <p role="alert">{state.discountError}</p> : null}
      {state.appliedDiscountCode ? (
        <button onClick={() => actions.removeDiscount()}>
          Remove {state.appliedDiscountCode}
        </button>
      ) : null}
    </div>
  );
}

CheckoutState fields

FieldTypeDescription
stepCheckoutStepActive step: cart | shipping | payment | result
statusCheckoutStatusCoarse engine status: idle | bootstrapping | working | redirecting | polling | error
locale'fa' | 'en'Active label and number-formatting locale
direction'rtl' | 'ltr'Active layout direction
cartCart | nullCurrent cart
invoiceInvoice | nullCurrent invoice
regionsCheckoutRegion[]Regions and nested cities used by the address form
addressFormAddressFormValuesLive shipping address form values
postalCodeMandatorybooleanWhether shop settings require a postal code
emailMandatorybooleanWhether shop settings require an email address
addressDirtybooleanWhether the form differs from the saved invoice address
applicableApplicableShippingMethods | nullRaw invoice-specific shipping response
shippingGroupsShippingGroup[]Grouped shipping rates for each item bundle
paymentMethodsPaymentMethod[]Available payment gateways
selectedPaymentMethodIdnumber | nullSelected gateway
discountCodestringInput value of the discount field
appliedDiscountCodestring | nullSuccessfully applied code
appliedDiscountAppliedDiscount | nullClassified percentage, fixed-amount, or free-shipping discount and its savings
discountErrorstring | nullInline error for the discount field (e.g. invalid code); cleared on edit/apply/remove
resultCheckoutResult | nullPost-payment result (status, order, message)
errorCheckoutError | nullLast error with message, code, step
flagsCheckoutFlagsGranular async flags (see below)

CheckoutFlags

FlagFires during
bootstrappingInitial cart + invoice load
updatingCartQuantity change or item remove
savingAddressShipping form submit
loadingShippingFetching applicable rates after address save
selectingRateSelecting a shipping rate
applyingDiscountDiscount code submission
loadingPaymentsFetching payment methods
placingOrderOrder placement + gateway redirect

CheckoutActions

MethodDescription
start()Bootstrap cart and invoice. Called automatically.
next()Advance to the next step (validates current step).
back()Return to the previous step.
goToStep(step)Jump to any step directly.
updateItemQuantity(cartProductId, variantId, quantity)Update a cart line quantity.
removeItem(cartProductId, variantId)Remove a cart line.
setAddressField(key, value)Update a single address form field.
submitAddress()Save address and load applicable shipping rates.
selectShippingRate(groupKey, rateId)Select a rate within a shipping group.
setDiscountCode(code)Update the discount code input.
applyDiscount()Submit the discount code.
removeDiscount()Remove the applied discount.
selectPaymentMethod(id)Select a payment method.
placeOrder()Place the order and redirect to the gateway.
resolvePaymentReturn(params)Resolve a return from the payment gateway.
reset()Reset checkout state to initial.

Using SazitoCheckout directly

SazitoCheckoutPage bundles the provider and UI. If you already have a CheckoutProvider in your tree, render SazitoCheckout directly:

import {
  CheckoutProvider,
  SazitoCheckout,
} from '@sazito/checkout/next';

// SazitoProvider must still wrap the tree above this component.
export default function CheckoutPage() {
  return (
    <CheckoutProvider
      credentials={{ cart: { identifier: '<cart-identifier>' } }}
      config={{ locale: 'fa' }}
    >
      <SazitoCheckout className="my-checkout" />
    </CheckoutProvider>
  );
}

SazitoCheckout includes the stepper, active checkout step, footer actions, and order summary. Use SazitoCheckoutPage unless you need to place the provider and UI separately.

Error codes

CheckoutError.code gives a machine-readable reason:

CodeMeaning
no_cartCart identifier not found
no_invoiceInvoice could not be created or found
min_basketCart total below store minimum
rate_limitedToo many requests
cart_invalidCart is empty or contains invalid items
invoice_lockedInvoice is locked (already placed or expired)
stock_violatedOne or more items went out of stock during checkout
shipping_requiredCart requires shipping but no address or rate provided
address_requiredShipping address missing
payment_failedGateway reported payment failure
networkNetwork or server error
validationInvalid input sent to the API
unknownUnclassified error

On this page