SazitoSazito SDKv1.2.5
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)
  • 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 (پایان خرید / "Finish purchase") finalizes the order directly — there is no separate review step.

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 four public 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/styles.cssSelf-contained RTL/LTR styles and theme variables.

Import the stylesheet once — typically in your root layout:

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

Setup

Create the Sazito SDK once and wrap your app (or checkout route) with SazitoProvider. Every SazitoCheckoutPage inside reads that same SDK client from context.

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

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

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

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

Basic usage

With SazitoProvider in place, the checkout page only needs credentials and config:

'use client';

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

export default function CheckoutPage() {
  return (
    <SazitoCheckoutPage
      credentials={{ cart: { identifier: '<cart-identifier>' } }}
      config={{
        locale: 'fa',
        continueShoppingUrl: '/',
        theme: { accent: '#4f46e5', radius: 16 },
      }}
    />
  );
}

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.
paymentReturnParamsRecord<string, string>Query params from a returning payment gateway. Triggers result-resolution mode instead of bootstrapping a fresh flow.
classNamestringExtra class on the root element.
renderNextButton(props: RenderButtonProps) => ReactNodeReplace the continue / next button across all steps.
renderBackButton(props: RenderButtonProps) => ReactNodeReplace the back button across all steps.
renderEmptyCart(props: RenderEmptyCartProps) => ReactNodeReplace the empty-cart screen. Receives continueShoppingUrl.

CheckoutCredentials

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

Typically only cart.identifier is needed. Supply invoice to re-attach to an already-created invoice (e.g. returning from a gateway).

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.
themeCheckoutThemeVisual overrides — see Theming.
continueShoppingUrlstringURL for the back button on the cart step and the "continue shopping" link on the result screen.
returnUrlstringcurrent URLURL the payment gateway redirects to.
pollIntervalMsnumber15000How often (ms) to poll for a pending payment result after gateway return.
currencyLabelstringlocale defaultOverride the currency unit label (e.g. 'تومان').
onEvent(event: CheckoutEvent) => voidAnalytics 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

When a gateway redirects back, render SazitoCheckoutPage with paymentReturnParams. The checkout auto-resolves the outcome and shows the result screen.

// app/checkout/return/page.tsx
import { SazitoCheckoutPage } from '@sazito/checkout/next';

export default function ReturnPage({
  searchParams,
}: {
  searchParams: Record<string, string>;
}) {
  return (
    <SazitoCheckoutPage
      credentials={{ cart: { identifier: '<cart-identifier>' } }}
      config={{ locale: 'fa', returnUrl: '/checkout/return' }}
      paymentReturnParams={searchParams}
    />
  );
}

If the gateway returns status: 'pending', the checkout polls every pollIntervalMs until it resolves.

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

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

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

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