🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@01.software/sdk

Package Overview
Dependencies
Maintainers
1
Versions
164
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@01.software/sdk

01.software SDK

Source
npmnpm
Version
0.37.0
Version published
Weekly downloads
685
1.63%
Maintainers
1
Weekly downloads
 
Created
Source

@01.software/sdk

Official TypeScript SDK for the 01.software platform.

Installation

npm install @01.software/sdk
# or
pnpm add @01.software/sdk

Features

  • Full TypeScript type inference
  • Browser and server environment support
  • React Query integration (both Client and ServerClient)
  • Mutation hooks (useCreate, useUpdate, useRemove) with automatic cache invalidation
  • Customer auth hooks (useCustomerMe, useCustomerLogin, etc.) with cache management
  • Automatic retry with exponential backoff (non-retryable: 400, 401, 403, 404, 409, 422)
  • Webhook handling with HMAC-SHA256 signature verification
  • Sub-path imports (./server, ./webhook, ./realtime, ./ui/*) for tree-shaking
  • Type-safe read-only collections.from() for Client (compile-time write prevention)

Sub-path Imports

// Analytics — browser pageview tracking + custom events
import { createAnalytics } from '@01.software/sdk/analytics'
const analytics = createAnalytics({ publishableKey: 'pk_xxx' })
// auto-tracks pageviews; call analytics.pageview('/custom-path') manually if needed
analytics.track('signup', { plan: 'pro', trial: false }) // custom event with optional props
// Analytics React component — Vercel Analytics-style mount helper
import { Analytics } from '@01.software/sdk/analytics/react'

export function App() {
  return <Analytics />
}
// Main entry - browser client, query builder, commerce helpers, utilities
import { createClient } from '@01.software/sdk'

// Server-only entry - keep Secret Key code out of browser-facing imports
import { createServerClient } from '@01.software/sdk/server'

// Webhook only - webhook handlers
import {
  handleWebhook,
  createTypedWebhookHandler,
} from '@01.software/sdk/webhook'

// Realtime only
import { createRealtimeClient } from '@01.software/sdk/realtime'

// Components - sub-path imports per domain
import { Analytics } from '@01.software/sdk/analytics/react'
import { RichTextContent } from '@01.software/sdk/ui/rich-text'
import { Image } from '@01.software/sdk/ui/image'
import { FormRenderer } from '@01.software/sdk/ui/form'
import { CodeBlock } from '@01.software/sdk/ui/code-block'
import { CanvasRenderer } from '@01.software/sdk/ui/canvas'
import { VideoPlayer } from '@01.software/sdk/ui/video'

The root entry keeps createClient, commerce helpers, collection helpers, and types lightweight. Server, React Query, and UI features live behind explicit sub-paths so consumers install feature peers only when they import the matching entry.

ImportFeature(s)Install when used
@01.software/sdkbrowser-safe createClient, commerce helpers, collection helpers, typesnone
@01.software/sdk/clientbrowser-safe createClient entrynone
@01.software/sdk/servercreateServerClient, server-only collection, commerce, and preview APIsnone; keep secretKey code on the server
@01.software/sdk/queryReact Query hooks, cache helpers, getQueryClient@tanstack/react-query, react, react-dom
@01.software/sdk/realtimeRealtimeConnection, useRealtimeQuery@tanstack/react-query, react, react-dom
@01.software/sdk/analytics/react<Analytics />react, react-dom
@01.software/sdk/ui/rich-textRichTextContent, StyledRichTextContentreact, react-dom, @payloadcms/richtext-lexical
@01.software/sdk/ui/formFormRendererreact, react-dom
@01.software/sdk/ui/code-blockCodeBlock, highlightreact, react-dom, shiki, hast-util-to-jsx-runtime
@01.software/sdk/ui/canvasCanvasRenderer, CanvasFrame, useCanvas, prefetchCanvasreact, react-dom, @tanstack/react-query, @xyflow/react, quickjs-emscripten, postcss, sucrase
@01.software/sdk/ui/canvas/servercanvas server helpersnone
@01.software/sdk/ui/videoVideoPlayerreact, react-dom, @mux/mux-player-react
@01.software/sdk/ui/imageImagereact, react-dom

If a feature is not listed here, it does not need a separate peer install. For the full component-to-peer mapping, see packages/sdk/.claude/rules/components-reference.md.

Migration quick reference:

  • createClient remains available from @01.software/sdk and @01.software/sdk/client.
  • createServerClient must be imported from @01.software/sdk/server.
  • React Query hooks and cache helpers must be imported from @01.software/sdk/query.
  • UI components must be imported from the specific @01.software/sdk/ui/* sub-path and require only that row's peers.
  • Console-shared pure ecommerce helpers live in private @01.software/contracts. The public SDK keeps customer-facing helpers self-contained and must not import private contracts; Console code should import shared helpers from contracts directly.

Getting Started

Client

import { createClient } from '@01.software/sdk'

const client = createClient({
  publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY,
})

// Query data (returns Payload native response)
const { docs } = await client.collections.from('products').find({
  limit: 10,
  where: { status: { equals: 'published' } },
})

Server Client

import { createServerClient } from '@01.software/sdk/server'
import { createServerQueryHooks } from '@01.software/sdk/query'

const server = createServerClient({
  publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY,
  secretKey: process.env.SOFTWARE_SECRET_KEY, // sk01_... opaque API key from Console
})
const serverQuery = createServerQueryHooks(server)

// Create order (server only)
const order = await server.commerce.orders.create({
  orderNumber: generateOrderNumber(),
  customerSnapshot: { email: 'user@example.com' },
  shippingAddress: { recipientName: 'John', phone: '010-1234-5678', postalCode: '12345', address: 'Seoul', detailAddress: 'Apt 101' },
  items: [{ product: productId, variant: variantId, option: optionId, quantity: 1 }],
  totalAmount: 10000,
  pgPaymentId: 'provider-payment-id', // optional (omit for free orders)
  discountCode: 'WELCOME10', // optional
})

// SSR prefetch (server)
await serverQuery.prefetchQuery({
  collection: 'products',
  options: { limit: 10 },
})

Create-order inputs prefer items; existing orderItems callers remain supported and are serialized to the same endpoint field. Order reads expose joined line items at order.items.docs.

Always import createServerClient from @01.software/sdk/server so generated code and bundlers do not blur the Secret Key boundary.

Server-rendered preview routes can use server.preview.detail() with the short-lived preview token issued by Console:

const preview = await server.preview.detail(
  { collection: 'products', id: previewId },
  { previewToken },
)

For product pages, server.commerce.product.previewDetail({ id }, { previewToken }) returns the raw product detail payload for the saved draft/unpublished record addressed by the preview token. detail() wraps the published storefront payload in a { found, product | reason } result.

Getting product detail

The recommended way to fetch a single product is the shaped helper:

import { createClient } from '@01.software/sdk'

const client = createClient({
  publishableKey: '<publishable-key>',
})

const result = await client.commerce.product.detail({
  slug: 'every-peach-tee',
})
if (!result.found) {
  return notFound()
}

const { product } = result
// product: { product, variants, options, brand, categories, tags, images, videos, listing }

detail() returns ProductDetailResult, a discriminated union: { found: true, product: ProductDetail } | { found: false, reason }. The reason value is one of not_found, not_published, or feature_disabled, so storefronts can choose between a standard 404, preview CTA, or feature gating UI. Permission/auth errors, including 403 tenant mismatches, still throw typed SDKError subclasses and preserve request IDs through the existing lastRequestId / onRequestId path.

The successful product payload exposes inventory rollups without sentinel values: product.totalInventory is the tracked stock sum across non-unlimited variants, null when no variants are tracked, and product.hasUnlimitedVariant signals whether any variant is unlimited.

Edge-cached catalog + live stock (storefront migration)

When a PDP or listing UI is served behind the Console Edge CDN, prefer the catalog/stock split instead of reading variant.stock from a cached detail() response (inventory in that payload can lag for the catalog TTL).

import {
  createClient,
  mergeProductDetailWithStock,
} from '@01.software/sdk'

const client = createClient({ publishableKey: '<publishable-key>' })

const catalog = await client.commerce.product.detailCatalog({ slug: 'every-peach-tee' })
if (!catalog.found) return notFound()

const variantIds = catalog.product.variants.map((v) => v.id)
const snapshot = await client.commerce.product.stockSnapshot({ variantIds })
const { product, stockMergeStatus } = mergeProductDetailWithStock(
  catalog.product,
  snapshot,
)
// stockMergeStatus: 'complete' | 'partial' — partial when a variant id is missing from snapshot

Listing groups use the same pattern: listingGroupsCatalog() for the cacheable shell, then stockSnapshot() (or stock-check at cart/checkout) for live availability. detail() and POST detail/listing endpoints are unchanged during the migration window; see ADR 0012 addendum in docs/decisions/0012-sdk-public-commerce-contract.md.

Product Listing Pages (PLP) — join-safe queries

Recommended path: Use commerce.product.listingGroupsCatalog() (or the useProductListingGroupsCatalogQuery hook) together with buildProductListingCard(). The endpoint applies unlimited join fetches server-side and returns pre-grouped ProductListingGroupsItem[] data, so color swatches are never truncated regardless of how many option values the product has.

import {
  buildProductListingCard,
  type ProductListingCard,
} from '@01.software/sdk'

const response = await client.commerce.product.listingGroupsCatalog({
  where: { status: { equals: 'published' } },
  limit: 24,
})

const cards: ProductListingCard[] = response.docs.map((item) =>
  buildProductListingCard(item, { basePath: '/shop' }),
)

Escape hatch: When you need to query the products collection directly (bulk operations, custom filters, fields the helper does not expose), spread PRODUCT_PLP_FIND_OPTIONS to raise the default Payload join limit of 10:

import {
  PRODUCT_PLP_FIND_OPTIONS,
} from '@01.software/sdk'

const { docs } = await client.collections.from('products').find({
  ...PRODUCT_PLP_FIND_OPTIONS,
  where: { status: { equals: 'published' } },
  limit: 24,
})

PRODUCT_PLP_FIND_OPTIONS sets joins.variants and joins.options to safe limits with sort: '_order'. It cures top-level products.options and products.variants join truncation but cannot cure the nested options[].values.docs join — the Payload REST joins param is flat and nested join limits require the listing-groups endpoint.

Product selection helpers

import {
  buildProductHref,
  buildProductOptionMatrixFromDetail,
  getProductSelectionImages,
  resolveProductSelectionFromMatrix,
} from '@01.software/sdk'

const matrix = buildProductOptionMatrixFromDetail(product)
const selection = resolveProductSelectionFromMatrix(
  matrix,
  { search: '?opt.color=black&opt.size=s' },
  undefined,
  { detail: product },
)

const images = getProductSelectionImages(selection) // object media only, deduped
const href = buildProductHref(product, {
  optionSlug: 'color',
  optionValueSlug: 'black',
})

Selection media follows the resolved selection: a complete variant uses that variant's media first; a partial option selection uses selected option-value media first, then matching variant media, before falling back to listing or product media. This keeps listing-card selection links and detail-page images aligned without rebuilding media priority in storefront code.

Commerce media note (pool + galleries)

For new storefront work, prefer pool-pointer and gallery-aware resolution from commerce.product.detail() + resolveProductSelection() (and getProductSelectionImages() when a list is needed). Direct legacy fields like variant.thumbnail and option-value direct images are still accepted as compatibility input, but are deprecated as primary storefront media sources.

availableValuesByOptionSlug / availableValuesByOptionId include availableStock, isUnlimited, and availableForSale per value so option UIs can render stock state without recalculating from variants. Each entry also exposes handoff-aligned aliases (exists == available, label == value) and an optional Shopify-shaped swatch: { color, image } alongside flat thumbnail and images. Only swatch.image falls back to the first entry in images when thumbnail is absent; flat thumbnail and images on the value object are unchanged from the matrix source. Option-value upsert and detail/matrix shapes use nested swatch only (swatch.color for hex); flat swatchColor is rejected.

With React Query

import { createQueryHooks } from '@01.software/sdk/query'

const query = createQueryHooks(client)
const { data: product, isLoading } = query.useProductDetailBySlug(slug)

Cache key is ['products', 'detail', { slug }]. Mutations on products, product-variants, product-options, product-option-values, brands, brand-logos, images, and related collections automatically invalidate this cache.

Selection URL contract

Use createProductSelectionCodec(detail) when product pages need to keep option selection in the URL. By default, complete selections emit variant=<variantId> and partial selections emit slug-compat params such as ?opt.color=ivory. Inbound canonical ID params (?opt.<optionId>=<valueId>) and compatibility slug params (?opt.<optionSlug>=<valueSlug>) still parse. Plain bare keys such as ?color=ivory are rejected.

import {
  createProductSelectionCodec,
  resolveProductSelection,
} from '@01.software/sdk'

const codec = createProductSelectionCodec(product)
const normalizedSelection = codec.parse('?opt.color=ivory')
const selection = resolveProductSelection(product, normalizedSelection)
const selectionQuery = codec.stringify(normalizedSelection)
// selectionQuery === 'opt.color=ivory' for partial selections
// selectionQuery === 'variant=variant-black-s' once a complete variant is selected
// selection.selectedVariant, selection.price, selection.stock, selection.media

Empty vs partial selection

When selection input is omitted, resolveProductSelection() applies listing.selectionHintVariant so PDP defaults match listing cards. That is separate from fillDefaults and is not gated by a flag.

// PDP default (uses listing.selectionHintVariant when selection is omitted)
resolveProductSelection(product)

// Catalog: keep price range / no concrete variant
resolveProductSelection(product, { valueIds: [] })

By default, partial selections (for example color only) leave selectedVariant as null. Opt in to Shopify-style selectedOrFirstAvailableVariant behavior with fillDefaults: true:

const resolution = resolveProductSelection(product, codec.parse('?opt.color=ivory'), {
  fillDefaults: true,
})
// resolution.selectedVariant is concrete; unselected options are filled
// using the same available-by-order rules as listing selectionHintVariant.

For option-click handlers, use selectNext() to apply a slug transition, keep compatible prior selections, and re-default incompatible ones. selectNext() already fills missing options internally:

import { resolveProductSelection, selectNext } from '@01.software/sdk'

const nextSelection = selectNext(product, currentSelection, 'color', 'ivory')
const resolution = resolveProductSelection(product, nextSelection)

Use fillDefaults: true on resolveProductSelection() when you have a partial URL or selection state and need a concrete variant without calling selectNext(). It does not change codec parse/stringify behavior.

Opt out of slug-compat outbound URLs with createProductSelectionCodec(product, { emit: 'canonical-id' }).

Normalized selection state uses stable option/value/variant IDs internally. Slugs in URLs are a compatibility/readability layer, not the identity source.

For listing cards, pass the listing group returned by buildProductListingGroupsByOption() or the listing-groups endpoint into buildProductHref(product, group, { detail }). Listing swatch hrefs emit partial slug-compat hints such as ?opt.color=ivory by default. When full detail is not available on a product-list page, pass the group without detail; buildProductHref() still emits the best available selection hint and the detail page can resolve it through resolveProductSelection().

Use preferCompleteVariantFromHint: true on buildProductHref() only when a listing card should deep-link a complete hint variant instead of a color-only partial hint.

Do not use bare option query keys such as ?size=large. The SDK rejects them as ambiguous because product pages commonly share URLs with unrelated search, filter, analytics, or framework parameters. Namespacing selection keys under opt. lets the codec distinguish product-option state from ordinary query parameters while still allowing unrelated parameters such as utm_campaign to coexist without being interpreted as selection state.

For SEO, treat the product path without selection params as the canonical URL. Selection query params are share/deep-link state, not index targets.

Product listing card helper

buildProductListingCard(item, options?) turns a single commerce.product.listingGroups() response item into a render-ready ProductListingCard. Each item includes listingGroupingState (grouped, no_primary_option, or empty) and, when empty, listingGroupingEmptyReason (primary_option_not_linked, primary_option_has_no_values, or no_variants_for_primary_option). Each group includes public-safe variants[] alongside variantIds/variantCount so storefronts can render or inspect grouped variant fields without a follow-up fetch. The by-ids response also returns missing: string[] for requested product IDs that were not found, not published, or not accessible; docs preserve the input productIds order for returned products. The helper populates optional representativeVariant, a PDP-seeded href, representative media (product.thumbnail -> first product.images -> representative variant media -> null), an aggregated price range across all option-value groups, and a swatches[] array derived from groups when there is more than one. Single-group products emit swatches: []; storefronts that disagree can read item.groups directly.

buildProductListingCard() derives card swatches from listing-group optionValueSwatch. Image swatches use swatch.mediaItemId; color swatches use swatch.color. Option-value thumbnail/gallery fields are no longer part of the public listing-group or product-detail contract.

import {
  buildProductListingCard,
  type ProductListingCard,
} from '@01.software/sdk'

const cards: ProductListingCard[] = response.docs.map((item) =>
  buildProductListingCard(item, { basePath: '/shop' }),
)

The card href is the product path by default; the PDP resolves the representative variant through resolveProductSelection(detail) without a selection param. Each swatch carries a hint-only slug-compat href such as ?opt.color=ivory; the detail page resolves it through resolveProductSelection(detail, { search }). Use preferCompleteVariantFromHint: true on buildProductListingCard() only when the card should deep-link a complete hint variant.

Storefront performance defaults

  • PLP: prefer useProductListingGroupsCatalogQuery() (GET /api/products/listing-groups/query/catalog, CDN-cacheable) or commerce.product.listingGroupsCatalog() + buildProductListingCard(). Use useProductListingGroupsQuery() when you need the full POST /query response shape. Avoid fetching a product list and then calling detail() per card.
  • PDP: prefer useProductDetailBySlug() / commerce.product.detail(). Override staleTime / retry on the hook when you need fresher catalog data or faster failure on errors.
  • CDN-friendly reads: server/edge code can use detailCatalog() and listingGroupsCatalog() (GET, cacheable) plus batched stockSnapshot() for live inventory.
  • React Query in the browser: default getQueryClient() keeps SSR data fresh forever (staleTime: Infinity). For client-only storefronts, use getStorefrontQueryClient() (~1 minute staleTime) when creating query hooks:
import { createClient } from '@01.software/sdk'
import { createQueryHooks, getStorefrontQueryClient } from '@01.software/sdk/query'

const client = createClient({ publishableKey: '...' })
const query = createQueryHooks(client, getStorefrontQueryClient())

Advanced: direct Payload queries (escape hatch)

Most consumers should use the helper APIs above (commerce.product.detail, etc.). The query builder below is the escape hatch for advanced cases the helpers do not cover: bulk operations, custom filter combinations, or fields the helper response does not expose.

depth — how deep to populate relationship fields

depth is the primary control for populating relationships like category, images, brand. The configured Payload default applies when unset.

const product = await client.collections.from('products').findById(id, {
  depth: 2, // populates product.category, product.category.parent, etc.
})

populate — which fields come back for populated relationships

populate controls which fields are returned per collection. It does NOT decide which relationships to populate — that is depth.

await client.collections.from('products').find({
  depth: 2,
  populate: {
    categories: { title: true, slug: true },
    images: { url: true, alt: true },
  },
})

joins — Payload join-field reverse-relations

joins is the correct control for Payload type: 'join' virtual reverse-relation fields. In this platform's public SDK schema, products.variants, products.options, customers.orders, customers.addresses, posts.comments, article-authors.articles, orders.{items,transactions,fulfillments,returns}, and similar reverse-relations are all join fields — you must use joins (not depth/populate) to control their pagination, sorting, filtering, and count. Internal backing joins such as product collection memberships are intentionally omitted from public SDK collection types.

// Canonical product detail query — variants/options are join fields on Products
await client.collections.from('products').find({
  where: { slug: { equals } },
  joins: {
    variants: { limit: 50, sort: '_order' },
    options: {},
  },
  depth: 2, // also populate normal relationships (category, brand, etc.)
})

// Disable all join-field population for a lightweight list query
await client.collections.from('products').find({
  joins: false,
})

Each join field defaults to limit 10 when joins is omitted. depth does not raise that cap — storefront PLPs that call products.find() with only depth and then buildProductListingGroupsByOption() can silently drop color swatches. Prefer listingGroupsCatalog() for PLP cards, or spread PRODUCT_PLP_FIND_OPTIONS for raw product queries (see PLP join-safe queries above).

joins does NOT populate normal relationship fields. Keys that do not match a type: 'join' field on the queried collection are silently ignored — e.g. joins: { category: {} } on Products is a no-op because category is not a join field there. For normal relationships use depth (and optionally populate).

Filtering by relation

Use id-based filters as the default — they're the most reliable:

await client.collections.from('product-variants').find({
  where: { product: { equals: productId } },
})

Dotted-path filters (where: { 'product.slug': { equals } }) are Payload-native but may silently return empty when access control restricts the related document or when the relation is polymorphic.

Why did my query return empty?

Checklist when find() returns docs: [] unexpectedly, in order of likelihood:

  • Access control filtered the document. Many collections enforce status/published filters on public read (e.g. composedReadStatusPublished on products restricts unauthenticated reads to status: 'published'). A draft or unpublished document silently disappears from results even when its slug matches. Correlate with backend logs via client.lastRequestId (or catch SDKError.requestId).
  • Build-time publishable key / API URL differs from runtime. SSG generateStaticParams / generateMetadata / the page render must all see the same tenant context. A wrong or missing key at build time produces a baked-in empty response.
  • Next.js SSG fetch cache served a stale empty response. Use cache: 'no-store' or export const revalidate = 0 on server components that should reflect live data.
  • where: { slug: 'x' } string shorthand. Always use { slug: { equals: 'x' } } — bare strings silently match nothing.
  • Wrong key in joins. Keys not matching a type: 'join' field on the queried collection are silently ignored (no error). For normal relationship fields use depth/populate, not joins.
  • Dotted-path relation filter (where: { 'category.slug': { equals } }) under polymorphic or access-control constraints — switch to id-based filter: where: { category: { equals: id } }.

Usage in Next.js SSG / Server Components

  • Create the client per request in server components. Avoid module-level singletons that could share state (customer token, cache) across unrelated requests.
  • depth impacts static generation cost. Deeper populates = larger build payloads. Use select/populate to trim response shape.
  • Cache interaction. SDK requests honor Next.js fetch caching. For pages that must reflect live data, set cache: 'no-store' or export const revalidate = 0 on the route segment, or pass per-fetch options if you proxy the SDK behind your own fetcher.
// app/products/[slug]/page.tsx
import { createClient } from '@01.software/sdk'

export const revalidate = 60 // ISR — adjust per page freshness need

export default async function ProductPage({ params }) {
  const client = createClient({
    publishableKey: '<publishable-key>',
  })
  const result = await client.commerce.product.detail({ slug: params.slug })
  if (!result.found) return notFound()
  const { product } = result
  // ...
}

API

Client Configuration

const client = createClient({
  publishableKey: string, // Required
  apiUrl?: string, // Optional API origin override
})

const server = createServerClient({
  publishableKey: string,
  secretKey: string, // sk01_... or pat01_...
  apiUrl?: string, // Optional API origin override
})
OptionTypeDescription
publishableKeystringAPI publishable key
secretKeystringAPI secret key or PAT (server only)
apiUrlstringOptional API origin override for staging, preview, or proxies

Use apiUrl: string when an SDK instance should target a non-default API origin.

API URL resolution order:

  • Explicit apiUrl passed to createClient() or createServerClient()
  • SOFTWARE_API_URL (server) or NEXT_PUBLIC_SOFTWARE_API_URL (browser)
  • Build-time default: DEFAULT_API_URL when injected at build time; otherwise dev-tagged SDK builds (-dev. versions) use https://api.stg.01.software, and regular releases use https://api.01.software

Query Builder

Access collections via client.collections.from(slug).

Note: the root client.collections.from() type exposes the lightweight read surface (find, findById, count). Metadata helpers live behind the optional @01.software/sdk/metadata entry, and write operations (create, update, remove, updateMany, removeMany) are only available on server.collections.from().

// List query - returns PayloadFindResponse
const { docs, totalDocs, hasNextPage } = await client.collections
  .from('products')
  .find({
    limit: 20,
    page: 1,
    sort: '-createdAt',
    where: { status: { equals: 'published' } },
    depth: 2,
    select: { title: true, slug: true },
  })

// Query with populate/joins control
const { docs } = await client.collections.from('products').find({
  select: { title: true, slug: true, price: true, thumbnail: true },
  joins: false, // disable joins for lightweight list
})

// Override relationship populate
const product = await client.collections.from('products').findById(id, {
  populate: { brands: { name: true, logo: true } },
  joins: { variants: { limit: 50 } },
})

// Single item query - returns document directly
const product = await client.collections.from('products').findById('id')

Raw collection mutations are an escape hatch. For ecommerce product catalog writes, prefer server.commerce.product.upsert() so options, option-values, and variants are written through the domain transaction.

// Create (server only) - returns PayloadMutationResponse
const { doc, message } = await server.collections
  .from('articles')
  .create({ title: 'Article' })

// Create with file upload (server only) - uses multipart/form-data
const { doc } = await server.collections
  .from('images')
  .create({ alt: 'Hero image' }, { file: imageFile, filename: 'hero.jpg' })

// Update (server only) - returns PayloadMutationResponse
const { doc } = await server.collections
  .from('articles')
  .update('id', { title: 'Updated article' })

// Update with file replacement (server only)
await server.collections
  .from('images')
  .update('id', { alt: 'New alt' }, { file: newFile })

// Delete (server only) - returns document directly
const deletedDoc = await server.collections.from('articles').remove('id')

// Count
const { totalDocs } = await client.collections.from('products').count()

// SEO Metadata (generate from a fetched document)
import { extractSeo, generateMetadata } from '@01.software/sdk/metadata'

const { docs } = await client.collections.from('products').find({
  where: { slug: { equals: 'my-product' } },
  limit: 1,
  depth: 1,
})
const metadata = docs[0]
  ? generateMetadata(extractSeo(docs[0]), { siteName: 'My Store' })
  : null

// Bulk operations (server only)
await server.collections.from('articles').updateMany(where, data)
await server.collections.from('articles').removeMany(where)

API Response Types (Payload Native)

The SDK returns Payload CMS native response types without wrapping:

// find() returns PayloadFindResponse<T>
interface PayloadFindResponse<T> {
  docs: T[]
  totalDocs: number
  limit: number
  totalPages: number
  page: number
  pagingCounter: number
  hasPrevPage: boolean
  hasNextPage: boolean
  prevPage: number | null
  nextPage: number | null
}

// create() / update() returns PayloadMutationResponse<T>
interface PayloadMutationResponse<T> {
  message: string
  doc: T
  errors?: unknown[]
}

// findById() / remove() returns T (document directly)
OperationResponse Type
find()PayloadFindResponse<T> - { docs, totalDocs, hasNextPage, ... }
findById()T - document object directly
create()PayloadMutationResponse<T> - { doc, message }
update()PayloadMutationResponse<T> - { doc, message }
remove()T - deleted document object directly
count(){ totalDocs: number }

React Query Hooks

React Query helpers are opt-in through @01.software/sdk/query. Install @tanstack/react-query (and React peers) only when your app imports this sub-path. Browser components should use createQueryHooks(client) for browser-safe reads and customer auth hooks. Collection writes belong in trusted server code via createServerClient.

import { createQueryHooks } from '@01.software/sdk/query'

const query = createQueryHooks(client)

// List query
const { data, isLoading } = query.useQuery({
  collection: 'products',
  options: { limit: 10 },
})

// Suspense mode
const { data } = query.useSuspenseQuery({
  collection: 'products',
  options: { limit: 10 },
})

// Query by ID
const { data } = query.useQueryById({
  collection: 'products',
  id: 'product_id',
})

// Infinite scroll
const { data, fetchNextPage, hasNextPage } = query.useInfiniteQuery({
  collection: 'products',
  options: { limit: 20 },
})

// SSR Prefetch
await query.prefetchQuery({
  collection: 'products',
  options: { limit: 10 },
})
await query.prefetchQueryById({
  collection: 'products',
  id: 'product_id',
})
await query.prefetchInfiniteQuery({
  collection: 'products',
  pageSize: 20,
})

// Cache utilities
query.invalidateQueries('products')
query.getQueryData('products', 'list', options)
query.setQueryData('products', 'detail', id, data)

// Customer auth hooks (Client only)
const { data: profile } = query.useCustomerMe()
const { mutate: login } = query.useCustomerLogin()
const { mutate: register } = query.useCustomerRegister()
const { mutate: logout } = query.useCustomerLogout()

login({ email: 'user@example.com', password: 'password' })

// Other customer mutations
query.useCustomerForgotPassword()
query.useCustomerResetPassword()
query.useCustomerChangePassword()

// Customer cache utilities
query.invalidateCustomerQueries()
query.getCustomerData()
query.setCustomerData(profile)
// Server action / API route for collection writes
import { createServerClient } from '@01.software/sdk/server'

const server = createServerClient({
  publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY!,
  secretKey: process.env.SOFTWARE_SECRET_KEY!,
})

await server.collections.from('articles').update('article_id', {
  title: 'Updated article',
})

Customer Auth

Customer auth methods currently cover local email/password flows: register, login, refresh, password reset, profile read/update, and password change. CustomerProfile.authProvider may contain google, apple, kakao, or naver for accounts created through platform/provider integrations, but the SDK does not expose social-login initiation or callback helpers yet.

Available on Client via client.customer.auth.*.

const client = createClient({
  publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY,
  customer: { persist: true },
})

// Register & login
const { customer } = await client.customer.auth.register({
  name: 'John',
  email: 'john@example.com',
  password: 'secure123',
})
const { token, customer } = await client.customer.auth.login({
  email: 'john@example.com',
  password: 'secure123',
})

// Profile & token management
const profile = await client.customer.auth.me()
client.customer.auth.isAuthenticated()
client.customer.auth.logout()

// Authenticated customer's own orders (Client-only)
const orders = await client.commerce.orders.listMine({
  page: 1,
  limit: 10,
  status: 'paid',
})

// Password
await client.customer.auth.forgotPassword('john@example.com')
await client.customer.auth.resetPassword(token, newPassword)
await client.customer.auth.changePassword(currentPassword, newPassword)

Commerce Orders (ServerClient-only writes)

Available on ServerClient via server.commerce.orders.*. Only checkout and listMine are also on Client.

// Orders
await server.commerce.orders.create(params)
await server.commerce.orders.update({ orderNumber, status: 'confirmed' })
await server.commerce.orders.checkout({ cartId, pgPaymentId?, orderNumber, customerSnapshot, discountCode? })

// Single-order lookup (getOrder endpoint removed — use collection find)
const { docs: [order] } = await server.collections.from('orders').find({
  where: { orderNumber: { equals: 'ORD-...' } },
  limit: 1,
  depth: 1,
})

// Fulfillment
await server.commerce.orders.prepareFulfillmentOrder({ orderNumber })
await server.commerce.orders.createFulfillment({ orderNumber, items })
await server.commerce.orders.updateFulfillment({ fulfillmentId, carrier, trackingNumber })
await server.commerce.orders.bulkImportFulfillments({ items: [{ orderNumber, carrier, trackingNumber }] })

// Provider-verified payment confirmation
// Provider webhook handlers should verify with the provider first, then call:
await server.commerce.orders.confirmPayment({
  orderNumber,
  pgProvider: 'provider-name',
  pgPaymentId,
  amount,
  providerStatus: 'PAID',
  providerEventId,
})

// Low-level transaction annotation / compatibility path. Prefer confirmPayment()
// for normal provider-verified paid transitions.
await server.commerce.orders.updateTransaction({ pgPaymentId, status, paymentMethod, receiptUrl })

// Returns
await server.commerce.orders.createReturn({
  orderNumber,
  returnItems: [{ orderItem, quantity, restockingFee? }],
  refundAmount,
  returnShippingFee?,
  initialShippingRefundAmount?,
  initialShippingRefundOverrideNote?,
  reason?,
})
await server.commerce.orders.updateReturn({ returnId, status })
await server.commerce.orders.returnWithRefund({
  orderNumber,
  returnItems: [{ orderItem, quantity, restockingFee? }],
  refundAmount,
  returnShippingFee?,
  initialShippingRefundAmount?,
  initialShippingRefundOverrideNote?,
  pgPaymentId,
})

Commerce Discounts / Shipping

Available on both Client (customer JWT) and ServerClient (secretKey).

await client.commerce.discounts.validate({ code, orderAmount })
await client.commerce.shipping.calculate({ shippingPolicyId?, orderAmount, postalCode? })
// calculateShipping returns: { shippingAmount, baseShippingAmount, extraShippingAmount, freeShipping, freeShippingMinAmount, isJeju, isRemoteIsland }

Commerce Product

Product reads are available on both Client and ServerClient via commerce.product.*. Product catalog writes are ServerClient-only. Use server.commerce.product.upsert() for product catalog writes that include options, option values, and variants. It is the tenant-admin safe path because it applies the product/option/variant transaction that raw collection writes do not provide.

commerce.product.upsert() is a graph-write API. For Admin Panel product documents, Payload saves product fields first; upsert receives productId, graphRevision (required when updating an existing product via productId - load from GET /api/products/:id/composer-draft), options, and variants. Do not send removed legacy media inputs (optionValue.thumbnail, optionValue.images, variant.thumbnail); use swatch.mediaItemId and variant images[]. Unknown keys are not part of the published upsert contract.

PayloadValidInvalid
Createproduct: { title, ... } + graphproductId on create; missing product.title
Edit graphproductId + graphRevision? + graphproduct.title / SEO on upsert; conflicting productId vs product.id
Edit (legacy)product: { id } + graph onlyproduct: { id, title } on edit
const result = await server.commerce.product.upsert({
  product: {
    title: 'Every Peach Tee',
    slug: 'every-peach-tee',
    status: 'published',
  },
  options: [
    {
      title: 'Color',
      slug: 'color',
      values: [
        {
          value: 'Black',
          slug: 'black',
          swatch: { type: 'color', color: '#111111' },
        },
        {
          value: 'White',
          slug: 'white',
          swatch: { type: 'color', color: '#ffffff' },
        },
      ],
    },
    {
      title: 'Size',
      slug: 'size',
      values: [
        { value: 'Small', slug: 's' },
        { value: 'Medium', slug: 'm' },
      ],
    },
  ],
  variants: [
    {
      optionValues: { color: { valueSlug: 'black' }, size: { valueSlug: 's' } },
      sku: 'TEE-BLK-S',
      price: 29000,
      stock: 10,
      isActive: true,
    },
    {
      optionValues: { color: { valueSlug: 'white' }, size: { valueSlug: 'm' } },
      sku: 'TEE-WHT-M',
      price: 29000,
      stock: 8,
      isActive: true,
    },
  ],
})

if (!result.ok) {
  throw new Error(result.message)
}

Existing product graph edits send the saved product id and the composer draft baseline, without product document fields:

await client.commerce.product.upsert({
  productId: 'prod_123',
  graphRevision: draft.graphRevision,
  options: draft.options,
  variants: draft.variants,
})

For updates to existing options or option-values, prefer id / valueId when available so rename-safe updates do not depend on slugs.

// Batch stock check (point-in-time read, NOT a reservation)
const { results, allAvailable } = await client.commerce.product.stockCheck({
  items: [{ variantId: '...', quantity: 2 }],
})

for (const item of results) {
  if (item.status === 'available' && item.isUnlimited) {
    // Unlimited stock is explicit; availableStock is a numeric count, not a sentinel.
    continue
  }

  if (item.status === 'not_published' || item.status === 'archived') {
    // Variant still exists, but its parent product is not currently sellable.
    continue
  }
}

Commerce Cart

Available on both Client and ServerClient via commerce.cart.*.

// Add item to cart
await client.commerce.cart.addItem({
  cartId,
  product,
  variant,
  option,
  quantity,
})

// Update item quantity
await client.commerce.cart.updateItem({ cartItemId, quantity })

// Remove item from cart
await client.commerce.cart.removeItem({ cartItemId })

// Other cart operations
await client.commerce.cart.get(cartId)
await client.commerce.cart.applyDiscount({ cartId, discountCode })
await client.commerce.cart.removeDiscount({ cartId })
await client.commerce.cart.clear({ cartId })

Community Moderation (ServerClient-only)

Available only on ServerClient via server.community.moderation.*.

await server.community.moderation.banCustomer({ customerId, isPermanent?, bannedUntil?, reason? })
await server.community.moderation.unbanCustomer({ customerId })

Webhook

Create or open the endpoint in Console → Integrations → Webhooks. After saving, use Reveal signing secret once for the initial value, or Rotate secret to invalidate the previous secret and receive a new one-time plaintext value. Set the copied value in the receiver environment as WEBHOOK_SECRET.

Webhook list/read responses do not include plaintext signing secrets — only the Console reveal/rotate flows return them. Use WebhookWithoutSecret and WebhookSigningSecretReveal from @01.software/sdk/webhook for typed clients. This value is not SOFTWARE_SECRET_KEY; multiple webhook endpoints may require multiple receiver secrets.

Use HMAC-SHA256 signature verification:

import { handleWebhook, createTypedWebhookHandler } from '@01.software/sdk'

const handler = createTypedWebhookHandler('orders', async (event) => {
  // event.data is typed as Order
  console.log(event.data.orderNumber)
})

export async function POST(request: Request) {
  const secret = process.env.WEBHOOK_SECRET
  if (!secret) throw new Error('WEBHOOK_SECRET is required')

  return handleWebhook(request, handler, {
    secret,
  })
}

// Signed deliveries include x-webhook-signature, x-webhook-timestamp,
// and x-webhook-delivery-id. handleWebhook rejects stale or unsigned
// deliveries when secret is set.
// Customer auth helper
import { createCustomerAuthWebhookHandler } from '@01.software/sdk/webhook'

async function sendPasswordResetEmail(
  email: string,
  resetPasswordToken: string,
): Promise<void> {
  console.log('Send password reset email', email, resetPasswordToken)
}

const customerAuthHandler = createCustomerAuthWebhookHandler({
  passwordReset: async ({ email, resetPasswordToken }) => {
    await sendPasswordResetEmail(email, resetPasswordToken)
  },
})
// Semantic order-change events keep operation as "update" for compatibility.
// Use isOrderChangedWebhookEvent when you need to distinguish manual ordering
// from content field edits.
import { handleWebhook, isOrderChangedWebhookEvent } from '@01.software/sdk/webhook'

function getWebhookSecret(): string {
  const secret = process.env.WEBHOOK_SECRET
  if (!secret) throw new Error('WEBHOOK_SECRET is required')
  return secret
}

export async function POST(request: Request) {
  return handleWebhook(
    request,
    async (event) => {
      if (isOrderChangedWebhookEvent(event)) {
        console.log(event.collection, event.change.scope, event.change.moved)
        return
      }
      console.log(event.collection, event.operation)
    },
    { secret: getWebhookSecret() },
  )
}

Orderable Admin Panel drag operations are delivered as operation: "update" with eventType: "collection.orderChanged". For join ordering, the webhook uses the public parent collection as collection and points event.change.moved at the public moved entity when one exists, so handlers do not need to depend on hidden Payload order fields or private backing rows. Customer group member ordering is currently treated as an unsupported hidden join-order surface and does not emit a semantic order-change webhook.

Supported Collections

Source of truth: packages/sdk/src/core/collection/const.ts (COLLECTIONS: 73).

CategoryCollections
Tenanttenants, tenant-metadata, tenant-logos
Productsproducts, product-variants, product-options, product-option-values, product-categories, product-tags, product-collections, brands, brand-logos
Ordersorders, order-items, returns, return-items, fulfillments, fulfillment-items, transactions
Customerscustomers, customer-profiles, customer-addresses
Cartscarts, cart-items
Commercediscounts, shipping-policies, shipping-zones
Contentdocuments, document-categories, document-types, articles, article-authors, article-categories, article-tags, links, link-categories, link-tags
Playlists / Tracksplaylists, playlist-categories, playlist-tags, tracks, track-categories, track-tags
Galleriesgalleries, gallery-categories, gallery-tags, gallery-items
Canvascanvases, canvas-node-types, canvas-edge-types, canvas-categories, canvas-tags, canvas-nodes, canvas-edges
Videosvideos, video-categories, video-tags
Live Streamslive-streams
Mediaimages
Formsforms, form-submissions
Communityposts, comments, reactions, reaction-types, bookmarks, post-categories, customer-profile-lists
Eventsevent-calendars, events, event-categories, event-occurrences, event-tags

Server-only collections: customer-groups, reports, and community-bans are available from createServerClient().collections for segmentation and moderation workflows, but are intentionally absent from browser collection discovery.

Utilities

resolveRelation

Resolves a Payload CMS relation field value. When depth is 0, relation fields return just an ID (number). When depth > 0, they return the full object. This utility normalizes both cases.

import { resolveRelation } from '@01.software/sdk'

const authors = post.authors?.map((a) => resolveRelation(a)) ?? [] // Author[]

Note: Prefer resolveRelation. It covers the same normalization use case directly.

generateOrderNumber

import { generateOrderNumber } from '@01.software/sdk'

const orderNumber = generateOrderNumber()
// "260121123456" (YYMMDDRRRRRR format)

formatOrderName

import { formatOrderName } from '@01.software/sdk'

formatOrderName([{ product: { title: 'Product A' } }])
// "Product A"

formatOrderName([
  { product: { title: 'Product A' } },
  { product: { title: 'Product B' } },
])
// "Product A 외 1건"

RichTextContent

React component for rendering Payload CMS Lexical rich text. Two variants:

  • RichTextContent — Base component with maximum flexibility (converters, blocks, nodeMap, disableDefaultConverters)
  • StyledRichTextContent — Headless component with slot-based customization (components prop)
import {
  RichTextContent,
  StyledRichTextContent,
  richTextNodeMap,
} from '@01.software/sdk/ui/rich-text'

// Base: full converter control
<RichTextContent
  data={content}
  className="prose"
  internalDocToHref={({ linkNode }) => `/articles/${linkNode.fields.doc?.value?.slug}`}
  blocks={{
    Iframe: ({ node }) => <iframe src={node.fields.url} />,
    Player: ({ node }) => <VideoPlayer url={node.fields.url} />,
  }}
/>

// Shared Payload view-map rendering for supported lightweight blocks
<RichTextContent data={content} nodeMap={richTextNodeMap} />

// Headless: component slots (Radix-style)
<StyledRichTextContent
  data={content}
  components={{
    Heading: ({ tag: Tag, children }) => (
      <Tag className={Tag === 'h1' ? 'text-4xl font-bold' : 'text-2xl'}>{children}</Tag>
    ),
    Link: ({ href, children, target, rel }) => (
      <a href={href} target={target} rel={rel} className="text-blue-600 underline">{children}</a>
    ),
    Upload: ({ src, alt, width, height }) => (
      <img src={src} alt={alt} width={width} height={height} className="rounded-lg" />
    ),
  }}
/>

Error Handling

The SDK throws typed errors instead of returning error responses:

import { isNetworkError, isApiError, isValidationError } from '@01.software/sdk'

try {
  const { docs } = await client.collections.from('products').find()
} catch (error) {
  if (isNetworkError(error)) {
    console.error('Network issue:', error.message)
  } else if (isApiError(error)) {
    console.error('API error:', error.status, error.message)
  }
}

Error classes: SDKError, ApiError, NetworkError, ValidationError, ConfigError, TimeoutError

Environment Variables

NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY=your_publishable_key
SOFTWARE_PUBLISHABLE_KEY=your_publishable_key
SOFTWARE_SECRET_KEY=sk01_...  # Server only — opaque API key from Console

secretKey should only be used in server environments. Never expose it to the browser.

secretKey may be an sk01_... API key or a pat01_... personal access token. Server SDK calls must also send the matching publishableKey; PAT tenant selection is pinned server-side, and callers must not send X-Tenant-Id.

API keys created without explicit scopes use the default ['read', 'write']. Console API-key creation can request narrower scopes[]; in the first explicit-scope slice tenant admins may grant only read and write. Higher-authority scopes such as webhook, analytics, and super-admin require platform authority and otherwise return scope_grant_forbidden.

SDK 0.9.0: Server auth now uses opaque bearer tokens (sk01_...). Generate API keys from the Console. createServerToken, createApiKey, and parseApiKey are no longer part of the SDK surface.

Changelog

v0.23.0 (Product option-value visuals)

  • Added reusable option-value visuals (nested swatch, thumbnail, images) to Payload types and ecommerce utility shapes.
  • Listing group summaries now include option-value visual metadata and can use one colorway image across every size variant.
  • Product/listing sellability now uses stock - reservedStock, matching checkout stock checks.

v0.34.0 (Breaking — swatch-only option-value visuals)

Breaking change. Option-value visual fields were collapsed into nested swatch (type, color, mediaItemId). Legacy public fields are removed from SDK input/output helpers and from listing/detail contracts.

SurfaceRemovedReplacement
Upsert option valueswatchColor, thumbnail, imagesswatch.type, swatch.color, swatch.mediaItemId
Listing groupsoptionValueSwatchColor, optionValueThumbnail, optionValueImagesoptionValueSwatch
Product detail option valueswatchColor, thumbnail, imagesswatch

Migration steps:

  • Replace color-only values with swatch: { type: 'color', color: '#111111' }.
  • Replace thumbnail/gallery values with swatch: { type: 'media', mediaItemId: '<product-pool-image-id>' }.
  • Ensure media swatches reference an image already attached to the parent product (products.images / products.thumbnail).
  • Remove reads of legacy response fields; consume optionValueSwatch / optionValue.swatch instead.

Migration Guide

v0.34.0 (Product option-value visuals — breaking)

See the v0.34.0 changelog entry above for the field mapping table and migration steps. There is no compatibility alias window for removed option-value visual fields; callers must migrate to nested swatch before upgrading.

v0.16.0 (Phase 1–7 sync — additive)

New error codes propagated via SDKError.code (no breaking change; existing callers ignore unknown codes safely):

CodePhaseTrigger
account_suspendedP1Suspended session / sk01_ / pat01_ / customer JWT — 401
pat_tenant_header_forbiddenP1pat01_ request carrying any X-Tenant-Id header — 401
tenant_mismatchP3Cross-tenant FK rejection (forms / community / orders)
server_derivedP3Body-driven write into a server-derived state field — 422
scope_deniedP5pat01_ whose ApiKeys.scopes lacks the operation

P5 also adds JWT-jti revocation: revokeCustomerJti(jti, ttl) on the server invalidates a token immediately; subsequent SDK calls receive 401 { code: 'token_revoked' }.

COLLECTIONS and INTERNAL_COLLECTIONS are now both exported from @01.software/sdk. Use INTERNAL_COLLECTIONS to detect admin-only slugs in custom tooling.

v0.8.0 (Breaking Changes)

Field renames — update any code that reads these fields from API responses:

CollectionOldNew
CustomerssocialIdproviderUserId
CustomersloginAttemptsloginAttemptCount
CustomersresetPasswordExpiryresetPasswordExpiresAt
Orders, CartsshippingFeeshippingAmount
CartsitemsTotalsubtotalAmount
TransactionspaymentIdpgPaymentId
DiscountstypediscountType
DiscountsvaluediscountValue
DiscountsusageLimitmaxUses
DiscountsusageCountusesCount
DiscountsperCustomerLimitmaxUsesPerCustomer
ShippingPoliciesbaseFeebaseAmount
ShippingPoliciesfreeShippingThresholdfreeShippingMinAmount
DocumentseffectiveDateeffectiveAt
DocumentsexpiryDateexpiresAt
ArticlesreadTimereadingMinutes
ApiUsagecountapiCallCount
ApiUsagestorageUsedstorageUsedBytes
ApiUsagetotalDocumentsdocumentCount

Collection renames:

  • order-productsorder-items
  • return-productsreturn-items
  • Removed: exchanges, exchange-products
  • Added: product-option-values

Boolean field renames (6 collections):

  • status: 'active' | 'inactive'isActive: boolean on Forms, ArticleAuthors, CustomerGroups, ShippingPolicies, ProductVariants

FAQs

Package last updated on 09 Jun 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts