Catalog npm
Back to packageRELEASE DETAILS

0.4.3-dev.1774320821495

npm

@01.software/sdk

01.software SDK

Published24 Mar 2026
Direct dependencies28
Transitive dependenciesNot calculated

README

Current package documentation

@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, ./storefront-cache, ./ui/*) for tree-shaking
  • Type-safe read-only collections.from() for Client (compile-time write prevention)

Sub-path Imports

// 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, isCommerceSDKError } from '@01.software/sdk/server'

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

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

// Storefront cache resource names for SSG/ISR adapters
import { storefrontCacheResources } from '@01.software/sdk/storefront-cache'

// Embedded app browser handshake
import { receiveEmbeddedAdminSession } from '@01.software/sdk/embedded-admin'

// Embedded app backend verification
import { verifyEmbeddedAdminSession } from '@01.software/sdk/embedded-admin/server'

// Components - sub-path imports per domain
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 entry and SDK error guardsnone
@01.software/sdk/servercreateServerClient, server-only APIs, and SDK error guardsnone; keep secretKey code on the server
@01.software/sdk/errorsSDK error classes and guardsnone
@01.software/sdk/metadataSEO metadata extraction and generation helpersnone
@01.software/sdk/webhookwebhook handlers, event guards, and webhook typesnone
@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/storefront-cacheproduct storefront cache resource name helpersnone
@01.software/sdk/embedded-adminembedded app browser handshake and shared protocol typesnone
@01.software/sdk/embedded-admin/serverassertion verification and JWKS loadingnone; keep receiver session creation on the server
@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

Video playback admission

On your server, call createServerClient(...).video.playback(video.id) after applying your viewer's content-access policy. Pass its streamUrl to <VideoPlayer video={video} streamUrl={streamUrl} />. The bearer URL expires after two hours; refresh it before the deadline during long playback or after a long pause. Free playback uses a quota-metered gateway and can return 403 when the monthly allowance is exhausted or 503 when admission cannot be verified. muxPlaybackId, getVideoStreamUrl, and raw Mux thumbnail/MP4 helpers are legacy public-ID paths for unmigrated paid assets. A signed asset's retained muxPlaybackId is historical usage evidence and is not a playable URL.

CanvasRenderer renders reserved shape nodes (terminator, process, decision, io, subprocess) with first-party SVG geometry, without a tenant catalog row or QuickJS template.

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

Embedded Admin App

An embedded admin app is one tenant-owned management UI rendered inside the Console Developer Settings surface. The browser helper accepts exactly one short-lived Console assertion through the pinned parent/origin challenge. Send the receipt immediately to your own backend; do not decode it into a browser session or persist it.

import { receiveEmbeddedAdminSession } from '@01.software/sdk/embedded-admin'

const receipt = await receiveEmbeddedAdminSession({
  consoleOrigin: 'https://console.01.software',
})

await fetch('/api/embedded-admin/session', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(receipt),
})

The receiver backend verifies the assertion against the Console JWKS and pins its own exact origin. Establish an app-local session only after this succeeds.

import { verifyEmbeddedAdminSession } from '@01.software/sdk/embedded-admin/server'

const claims = await verifyEmbeddedAdminSession({
  channelId: body.requestId,
  consoleOrigin: 'https://console.01.software',
  destination: 'https://admin.example.com',
  token: body.token,
})

Tenant admins configure the active tenant's single binding through the Human CLI (01 embedded-app configure) with a user-scoped PAT. Binding management is an operator control-plane action and is intentionally not exposed as a public SDK mutation facade. A tenant API key (sk01_) is rejected.

Loopback HTTP is disabled by default. Only non-production fixtures may set allowLoopbackHttp: true on the receiver and verifier options.

The assertion proves operator and tenant context only. It is not a Console API credential; the external backend must use its own server-held credential and authorization policy for tenant data.

Migration quick reference:

  • createClient remains available from @01.software/sdk and @01.software/sdk/client.
  • createServerClient must be imported from @01.software/sdk/server.
  • Error guards are available from the same @01.software/sdk/client or @01.software/sdk/server entry as the client that throws; the root and @01.software/sdk/errors guards also classify errors across entries.
  • React Query hooks and cache helpers must be imported from @01.software/sdk/query.
  • Product storefront cache resource helpers must be imported from @01.software/sdk/storefront-cache.
  • 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.

For website analytics, integrate an external tool such as GA4, Plausible, or Vercel Analytics in your own frontend. The SDK no longer provides analytics collection helpers.

Getting Started

Client

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

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

// Fetch public-safe, card-ready catalog data through the shaped helper.
const page = await client.commerce.product.listingPage({
  limit: 10,
})
const cards = page.cards

For Payload-native reads that shaped helpers do not cover, use the advanced raw collection escape hatch.

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)

// Start the canonical Cart -> Checkout -> PaymentSession flow.
const { cart, cartToken } = await server.commerce.carts.create({ market: 'kr' })
await server.commerce.carts.addItem({
  cartToken,
  productId,
  variantId,
  quantity: 1,
  expectedRevision: cart.revision,
  idempotencyKey: crypto.randomUUID(),
})
const checkout = await server.commerce.checkouts.create({
  cartToken,
  customer: { email: 'user@example.com' },
  idempotencyKey: crypto.randomUUID(),
})

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

Provider-paid and manual Order creation are not part of the 0.45 SDK surface. An Order is emitted only when the Checkout is placed. Canonical Order reads expose line snapshots directly at order.items.

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

Tenant context introspection

Trusted server workflows can inspect the resolved tenant capability context without calling the Console endpoint directly:

const context = await server.tenant.context()
const productSchema = await server.tenant.collectionSchema('products')

console.log(context.features)
console.log(context.collections.active)
console.log(context.fieldConfigs)
console.log(productSchema.collection.customFields)

Pass { includeCounts: true } only when document counts and webhook configuration are needed. That mode bypasses the endpoint cache and performs additional collection reads. Tenant introspection is server-only and is not exported from the browser-safe root entry.

collection.customFields is optional and currently applies to customers and products. It exposes storagePath: 'customData', active definition machine keys and types, optional required: true, and select option storage values. Operator labels and stored document values are excluded; this schema metadata does not grant collection read or write access.

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 storefront content

Use shaped content helpers when a browser storefront needs relationship-backed media for common public content. These helpers use the publishable key only; the server resolves the tenant, enforces the owning feature, excludes drafts, and returns allowlisted DTOs instead of raw Payload documents.

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

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

const links = await client.content.links.list({
  limit: 10,
  categorySlug: 'social',
  tagSlug: 'footer',
  featured: true,
  sort: 'title',
})

const gallery = await client.content.galleryItems.list({
  gallerySlug: 'spring-lookbook',
  limit: 24,
})

content.links.list() calls GET /api/links/storefront; link DTOs include display fields, categories/tags, thumbnail, and icon media. Operator-only fields such as tenant, metadata, click counters, and private storage details are omitted. Use categorySlug and tagSlug for stable storefront URLs when available; categoryId and tagId are also supported. Expired links are excluded by default, though visible link DTOs may include expiresAt so storefronts can render time-sensitive copy.

content.galleryItems.list() calls GET /api/gallery-items/storefront; gallery item DTOs include title, description, content, gallery reference, and optional mediaType (image or video). Treat an omitted mediaType as image for older responses. Image items retain image media. Video items include a safe video descriptor with availability: 'ready' | 'processing' | 'unavailable', and may include a video document id and uploaded thumbnail. Disabled or plan-blocked video features, missing/deleted targets, and failed videos return only { availability: 'unavailable' }, preserving each item's position and pagination. The image relationship is not used as a video poster. Tenant, metadata, draft status, and storage/provider internals are omitted. Gallery item reads require either gallerySlug or galleryId; prefer gallerySlug for storefront routes. The default gallery item sort is manual, matching curator order from the Console.

The descriptor's ready state is preparation metadata, not playback authority. Your application server must verify that the requested video belongs to the published gallery/item and apply its viewer policy before calling server.video.playback(video.id). Deliver that response's streamUrl to the player; playback rechecks the current tenant, video state, and feature/plan. Keep server credentials on the application server. Gallery responses do not include playback URLs, provider IDs, or tokens.

Both helpers return Payload-style pagination (docs, totalDocs, page, limit, etc.). limit is bounded server-side to 1..100; invalid query, publishable-key, feature, rate-limit, and server errors surface through the SDK's typed error classes and preserve request IDs on client.lastRequestId. SDK sort inputs are typed public allowlists: links support created/updated/ published dates, title, and featured ordering; gallery items support manual curator order, created/updated dates, and title.

Use client.collections.from(...).find() only as the advanced raw collection escape hatch. Browser publishable-key raw reads stay shallow (depth: 0, joins: false) and are not for relationship-expanded media. Use shaped helpers for storefront content media, or createServerClient() when a server route needs full raw collection access with server credentials.

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,
//   featuredImage, priceRange, compareAtPriceRange, availableForSale, selectedOrFirstAvailableVariant }

Browser detail() returns ProductDetailCatalogResult, the same public-safe catalog detail shape as detailCatalog(): { found: true, product: ProductDetailCatalog } | { 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.

Public product visibility is driven by product status and publishedAt. Listing helpers return products with status: 'published' when publishedAt is current or empty. Direct detail and checkout admission also allow status: 'unlisted', so an unlisted product can be opened by slug/id without appearing in PLP/search/curated listing helpers. Draft, archived, scheduled, or malformed lifecycle states return { found: false, reason: 'not_published' } from detail and are omitted from listings.

The successful browser product payload omits operational inventory fields such as product.totalInventory, variant stock, and variant reservedStock. Use stockSnapshot() / stockCheck() for live storefront inventory overlays, or use createServerClient().commerce.product.detail() from trusted server code when a full operational detail read is required.

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 treating cached catalog responses as live inventory authorities.

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 UIs use the same pattern: listingPage() for PLP/search grids, or listingGroupsCatalog({ productIds }) when curated product IDs are already known, then stockSnapshot() (or stock-check at cart/checkout) for live availability. Browser detail() is now catalog-safe as well; 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.listingPage() (or createQueryHooks(client).useProductListingPage()) for greenfield storefront PLPs. It wraps the cacheable listing-groups query endpoint, keeps the raw Payload pagination response, and adds cards built with buildProductListingCard(). The endpoint returns pre-grouped listing data and avoids the top-level products.options / products.variants join truncation that raw REST product queries hit by default.

const response = await client.commerce.product.listingPage({
  search: 'shirt',
  limit: 24,
  filters: {
    categoryIds: ['category-1'],
    price: { min: 10000, max: 50000 },
    availableForSale: true,
  },
  basePath: '/shop',
})

const cards = response.cards

Use commerce.product.listingGroupsCatalog({ productIds }) when product IDs are already known, for example curated rails, recommendations, or editorial sections:

const response = await client.commerce.product.listingGroupsCatalog({
  productIds: ['product-1', 'product-2'],
})

Server-auth escape hatch: When server code deliberately needs raw products collection reads (bulk operations, custom filters, fields the helper does not expose), use createServerClient() and spread PRODUCT_PLP_FIND_OPTIONS to raise the default Payload join limit of 10:

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

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

const now = new Date().toISOString()
const { docs } = await server.collections.from('products').find({
  ...PRODUCT_PLP_FIND_OPTIONS,
  where: {
    and: [
      { status: { equals: 'published' } },
      {
        or: [
          { publishedAt: { less_than_equal: now } },
          { publishedAt: { exists: false } },
          { publishedAt: { equals: null } },
        ],
      },
    ],
  },
  limit: 24,
})

const listingProducts = docs.map(projectProductToListingShape)

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. This preset is not accepted by publishable createClient().collections.from('products').find() because browser-public raw reads are constrained to depth: 0 and joins: false, and cannot use populate.

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 pre-ADR-0025 fields like variant.thumbnail and option-value direct images are still accepted as transitional input, but are no longer 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 selectedOrFirstAvailableVariant so PDP defaults match listing cards. That is separate from fillDefaults and is not gated by a flag.

// PDP default (uses selectedOrFirstAvailableVariant 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 rule that derives selectedOrFirstAvailableVariant.

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.listingPage() or listingGroupsCatalog() 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 from Shopify-shaped product.featuredImage with product gallery fallback, Product-level priceRange / compareAtPriceRange, Product-level availableForSale, 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. New PLP filters and sorts should use Product-shaped names such as priceRange.minVariantPrice.amount, priceRange.maxVariantPrice.amount, and availableForSale.

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.

Memberships

Membership self-service is split by credential boundary:

  • Browser/customer JWT clients can read the signed-in customer's current or latest membership and request cancellation for active, trialing, or past-due memberships.
  • Server clients with a Secret Key prepare a paid customer subscription before the tenant's PSP executes the first charge.
import { createClient } from '@01.software/sdk'
import { createServerClient } from '@01.software/sdk/server'

const client = createClient({ publishableKey: 'pk01_...' })

const membership = await client.memberships.me()
if (
  membership &&
  ['active', 'trialing', 'past_due'].includes(membership.status)
) {
  await client.memberships.cancel({ id: membership.id })
}

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

const prepared = await server.memberships.subscribe({
  planId: 'plan_123',
  priceId: 'price_monthly_123',
  customerId: 'customer_123',
  customerRef: 'cus_provider_123',
  subscribeIntentKey: 'checkout-session-123',
  card: { last4: '4242', brand: 'visa' },
})

if (prepared.status === 'incomplete') {
  await tenantPsp.charge({
    idempotencyKey: prepared.firstCharge.idempotencyKey,
    amount: prepared.firstCharge.amount,
    currency: prepared.firstCharge.currency,
  })
  // Post the PSP outcome to Console's existing signed charge-result webhook.
}

subscribeIntentKey is sent as X-Idempotency-Key; it is not part of the JSON body. A paid response is incomplete and contains the immutable first-charge instruction. The tenant charges its PSP with firstCharge.idempotencyKey, then posts the signed outcome through the configured charge-result webhook. A trial response is immediately trialing and has no firstCharge field. Before a paid subscribe, configure an active delegated billing connection for the tenant; otherwise Console returns 409 conflict without lifecycle writes. Do not start the PSP payment at or after expiresAt; deduplicate PSP work with firstCharge.idempotencyKey. A late paid result remains evidence but cannot activate the Membership, so the tenant handles any refund outside this API. Replays use the same plan, price, customer reference, and idempotency key while the first charge is pending. A replay after settlement or expiresAt returns 409 instead of a payment instruction. memberships.me() and browser memberships.cancel() require the customer JWT configured on the client. Refund webhook refundedAmount values are cumulative charge totals, not per-event deltas.

Source-aware customer groups (server only)

Manage manual Customer Group membership through the shaped server API. The read result includes every current reason a customer qualifies, so revoking a manual source does not hide a still-active Membership source.

const page = await server.customerGroups.listMembers({
  groupId: '00000000-0000-4000-8000-000000000002',
  limit: 100,
})

await server.customerGroups.grantMember({
  groupId: '00000000-0000-4000-8000-000000000002',
  customerId: '00000000-0000-4000-8000-000000000003',
  reason: 'support-approved',
})

await server.customerGroups.revokeMember({
  groupId: '00000000-0000-4000-8000-000000000002',
  customerId: '00000000-0000-4000-8000-000000000003',
})

Keep these calls in trusted server code. Do not mutate customers.groups directly: after source-authority activation it is a read-compatible projection and direct writes return customer_groups_direct_write_retired.

server.externalCustomerLinks connects a platform Customer to an external Customer ID within the namespace and environment derived from an active integration registration. Your app supplies a normalized stable accountScope to reserve identity; namespace-bound keys authorize all accounts in that namespace, and account scope is not an account ACL. All six methods (connect, get, list, unlink, capturePreviousState, clearPreviousState) require server credentials and Customer read or write scope.

const link = await server.externalCustomerLinks.connect({
  integrationId,
  customerId,
  accountScope,
  externalCustomerId: 'contact-123',
  requestId: crypto.randomUUID(),
})
const capture = {
  integrationId,
  customerId,
  linkId: link.id,
  requestId: crypto.randomUUID(),
  expectedVersion: link.version,
  previousState: { tier: 'silver' },
}
await server.externalCustomerLinks.capturePreviousState(capture)
// After an interrupted response, retry this exact request, including expectedVersion.

The bounded previous state accepts up to 16 flat string/number/boolean/null fields, 512 characters per string, and 2048 UTF-8 JSON bytes. It records only what your server supplies; the platform performs no external provider I/O. Before restoring provider state, check the current active owner with list({ integrationId, accountScope, externalCustomerId, state: 'active' }) and evaluate whether the restoration is still appropriate. Lists accept optional Customer, account scope, external ID, and state filters (external ID requires account scope), limit (default 25, maximum 100), and afterId; use nextCursor for the next page. Unlink and clear also require a fresh request UUID and the current version; exact retries retain the original values. A privacy-erased snapshot has state erased, null externalCustomerId, and null previousState. Keep these calls and snapshots in trusted server code.

Storefront performance defaults

  • PLP: prefer commerce.product.listingPage() or useProductListingPage() (GET /api/products/listing-groups/query/catalog, CDN-cacheable, card-ready). Use listingGroupsCatalog({ productIds }) only when IDs are already known. Treat the full listing-groups response shape as a server-auth escape hatch because it can include operational stock fields. 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. Browser publishable-key raw collection reads are constrained to depth: 0 with joins: false and no populate; relationship-rich storefront reads should use shaped helpers such as commerce.product.detail() / listingPage(), or a createServerClient() raw query when server credentials are appropriate. Browser SDK raw reads add those safe defaults automatically and reject relationship-expanded raw read options before making a request.

const product = await client.collections.from('products').findById(id, {
  depth: 0,
  joins: false,
})

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 server.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 SDK schema, browser-public relations such as products.variants, products.options, and article-authors.articles, plus server-auth relations such as customers.orders, customers.addresses, posts.comments, and orders.{items,transactions,fulfillments,returns}, 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 SDK collection types.

// Canonical product detail query — variants/options are join fields on Products
await server.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({
  depth: 0,
  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 listingPage() for PLP cards, or use PRODUCT_PLP_FIND_OPTIONS only in server-auth raw product queries (see PLP join-safe queries above).

Publishable-key browser raw reads must keep depth: 0, joins: false, and omit populate; relationship-expanded public storefront reads belong behind shaped helpers. Use createServerClient() for raw joins queries that need server credentials.

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:

  1. Access control filtered the document. Many collections enforce public read filters. Product listing reads require status: 'published' plus a current or unset publishedAt. Draft, future, unlisted, archived, or malformed products silently disappear from raw listing results even when their slug or ID matches. Use commerce.product.detail({ slug | id }) for unlisted products. Correlate with backend logs via client.lastRequestId (or catch SDKError.requestId).
  2. 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.
  3. 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.
  4. where: { slug: 'x' } string shorthand. Always use { slug: { equals: 'x' } } — bare strings silently match nothing.
  5. 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.
  6. 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
  // ...
}

The SDK sends Accept: application/vnd.01software.portone-action-capabilities.v1+json for Standard create and browser/server PortOne retrieve. Hosted create and browser/server cancel do not send it. It accepts either the negotiated exact action or the released Console's exact action, normalizing the latter to cancelable: false and retryPolicy: 'same_action_only'. Partial capability fields, different literals, and unknown response fields still fail closed. The Accept value must be that sole exact media type after optional surrounding whitespace. Lists, wildcards, parameters, and q-values deliberately retain the default projection; this makes either Console-first or SDK-first rollout safe. Direct HTTP create/retrieve responses use the vendor Content-Type only when the successful body actually contains the Standard capability action. Hosted, Toss, cancel, confirm, and error responses remain application/json.

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:

  1. Explicit apiUrl passed to createClient() or createServerClient()
  2. SOFTWARE_API_URL (server) or NEXT_PUBLIC_SOFTWARE_API_URL (browser)
  3. 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',
    depth: 0,
    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 and join expansion (server credentials only)
const product = await server.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')

Localized raw collection reads can request a supported Payload locale. The SDK currently exposes en and ko; it does not expose Payload's locale=all shape. fallbackLocale serializes to Payload's REST fallback-locale query key, and fallbackLocale: false serializes as fallback-locale=none.

const { docs } = await client.collections.from('articles').find({
  locale: 'ko',
  fallbackLocale: 'en',
  limit: 10,
})

const article = await server.collections.from('articles').findById(id, {
  locale: 'ko',
  fallbackLocale: false,
})

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: 0,
})
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)

Collection find, count, updateMany, and removeMany share filter validation. equals: null and not_equals: null retain their null meaning through Payload REST, including text fields; null-containing filters use Payload's native JSON where query parameter. Other filters keep bracketed encoding. The SDK throws ValidationError before a request for undefined values, null outside those two operators, empty condition objects, empty arrays (including and/or), nesting beyond 10 levels, and arrays with more than 1000 elements. Omit where for an unfiltered read. For a dynamically empty ID list, return an empty result before calling the SDK rather than passing in: []. False, zero, empty strings, and Date conditions remain valid.

Check your bulk filters carefully: calls that previously failed with HTTP 400 now perform updates or deletions. Large in/not_in lists can exceed URL length limits (around 250 UUIDs); split work into smaller batches. For not_in, keep the full exclusion condition in every batch rather than splitting the exclusions across separate mutations.

Orders update compatibility

Orders accept one-document updates only. Admin Panel bulk edits and generic server.collections.from('orders').updateMany(...) are rejected before any Order changes with HTTP 400 (errors[0].data.code: order_bulk_update_unsupported). Use update(orderId, data) for an unconditional edit. For a conditional update, keep the entire where predicate in the raw PATCH /api/orders query and set limit=1; send the field data as the body. Local API callers use numeric limit: 1, or a strict UUID id.equals that bounds the complete predicate. An ID in only one OR branch is insufficient. SDK updateMany has no limit option and is not a supported Orders CAS path.

A conditional miss returns docs: [], errors: []. A collected hook failure instead returns HTTP 500 (errors[0].data.code: order_update_failed), with a localized message and rollback of the supported single operation. Access and tenant checks remain in force. Do not replace a conditional update with an unconditional ID update or automatically retry payment capture. Separate per-order edits do not promise atomic success across the whole selection.

Console Local API Orders updates also require a live transaction. Setting disableTransaction: true without a live external transaction rejects both top-ID and where updates before writes with HTTP 400 (errors[0].data.code: order_transaction_required). Remove that option to use Payload's normal transaction, or pass a live external transaction; a request transaction ID by itself is not sufficient.

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

New storefronts should use hosted customer OAuth through client.customer.oauth.*. The SDK generates PKCE authorization URLs, parses the callback, exchanges codes, rotates refresh tokens, and calls hosted logout, but it does not store tokens for you. Store OAuth access, refresh, and transient state in trusted route handlers, preferably HttpOnly cookies or a server-side session store. Do not put OAuth tokens in browser-readable storage.

const client = createClient({
  publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY,
  customer: {
    persist: false,
    oauth: {
      issuer: process.env.NEXT_PUBLIC_SOFTWARE_CUSTOMER_AUTH_ISSUER!,
      clientId: process.env.NEXT_PUBLIC_SOFTWARE_CUSTOMER_CLIENT_ID!,
    },
  },
})

const customerOAuth = client.customer.oauth
if (!customerOAuth) throw new Error('Customer OAuth is not configured')

const authorization = await customerOAuth.createAuthorizationUrl({
  redirectUri: `${origin}/api/auth/customer/callback`,
  returnTo: '/account',
})

const callback = customerOAuth.parseCallback(callbackUrl, {
  expectedState: authorization.state,
})

const tokens = await customerOAuth.exchangeCode({
  code: callback.code,
  codeVerifier: authorization.codeVerifier,
  redirectUri: `${origin}/api/auth/customer/callback`,
})

await customerOAuth.refresh({ refreshToken: tokens.refreshToken })
await customerOAuth.logout({ refreshToken: tokens.refreshToken })

exchangeCode() may return an idToken when the openid scope is granted. In this SDK release, treat idToken as opaque unless your server verifies issuer, audience, expiry, nonce, and hosted JWKS signature itself. Use me() after establishing an access token for trusted customer profile display.

Legacy direct local auth

client.customer.auth.* remains available for existing local email/password flows: register, login, refresh, password reset, profile read/update, and password change. Keep this path as explicit legacy compatibility for existing integrations; do not overload login() to sometimes redirect into hosted 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()

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

await client.customer.auth.logoutAsync()

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

logoutAsync() revokes the direct refresh-token family on the server and clears local credentials even when the request fails. Handle a rejection as incomplete server revocation. The older synchronous logout(): void clears local state immediately but cannot revoke the server session. useCustomerLogout() awaits the server request and clears authenticated customer query data on success or failure. An issued access token can remain valid for up to 15 minutes; legacy bearer-only refresh remains a separate compatibility path.

forgotPassword() keeps the same SDK signature in both delivery modes. A tenant administrator chooses either backward-compatible signed webhook delivery or platform-managed email in the Console customer-authentication settings. Platform email requires the storefront reset URL to be configured; the platform appends the token query parameter and sends through the verified tenant sender or the platform fallback. Do not add reset tokens or dynamic redirect URLs to the SDK call.

Self-withdrawal

A signed-in customer with a local password can withdraw using their current password. With an authenticated client:

const { withdrawnAt } = await client.customer.auth.withdraw(currentPassword)

This calls POST /api/customers/withdraw with the publishable key and Customer JWT. Success archives the account immediately and clears the SDK's local credentials. Clear any application-owned session cookies and customer cache as well. Archived accounts cannot use existing sessions or authenticate again. Withdrawal preserves order, payment and membership-charge history; it does not erase personal data or free the email for a new registration.

Branch on SDKError.apiReason: active_membership_exists (409) requires the membership to end first, including scheduled cancellations still in an active status; reauthentication_unavailable (409) means no local password is set and the customer should contact the tenant administrator. Incorrect passwords use the shared login attempt counter and temporary lock (account_locked, 423). Refusal leaves the SDK credentials intact. Server keys cannot withdraw on a customer's behalf. Operator restore continues to follow the existing archive rules; the withdrawal timestamp is retained.

Market discovery

Discover a tenant's active buyer markets and resolve the default market. These reads are safe for a publishable key — server-only FX/pricing fields are never returned.

const markets = await client.markets.list() // MarketDTO[] (active markets)
const primary = await client.markets.default() // MarketDTO | null (active primary)

// Pass the resolved handle to market-aware commerce calls:
const cart = await client.commerce.carts.create({ market: primary?.handle })

MarketDTO is { id, handle, name, countryCode, targetCountries, currency, isPrimary, isActive } — countryCode/targetCountries are ISO 3166-1 alpha-2, currency is ISO 4217.

client.markets.default() resolves the tenant's primary market from server data. It is distinct from the client's configured default market handle (CommerceClient.getDefaultMarket()), which is the handle you passed to createClient({ market }).

Commerce Market Context

Pass market when constructing a client to default market-aware product and cart helpers to that market. Per-call market still wins when supplied.

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

await client.commerce.product.detail({ slug: 'shirt' }) // uses kr
await client.commerce.carts.create() // creates a kr cart
await client.commerce.product.detail({ slug: 'shirt', market: 'us' }) // override

const usClient = client.withMarket('us')
const { cart } = await usClient.commerce.carts.retrieve({ cartToken })
await usClient.commerce.carts.addItem({
  cartToken,
 

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Declared dependencies 34

Native requirements
28 direct targetsNot calculated transitive count
Package / targetRequirementRelationship
jose
Environment
{
  "local_name": "jose",
  "alias": false,
  "peer_metadata": null
}
^6.1.3runtime
shiki
Environment
{
  "local_name": "shiki",
  "alias": false,
  "peer_metadata": null
}
^4.0.1runtime
qs-esm
Environment
{
  "local_name": "qs-esm",
  "alias": false,
  "peer_metadata": null
}
^7.0.2runtime
payload
Environment
{
  "local_name": "payload",
  "alias": false,
  "peer_metadata": null
}
>=3.78.0runtime
sucrase
Environment
{
  "local_name": "sucrase",
  "alias": false,
  "peer_metadata": null
}
^3.35.1runtime
hast-util-to-jsx-runtime
Environment
{
  "local_name": "hast-util-to-jsx-runtime",
  "alias": false,
  "peer_metadata": null
}
^2.3.6runtime
@payloadcms/richtext-lexical
Environment
{
  "local_name": "@payloadcms/richtext-lexical",
  "alias": false,
  "peer_metadata": null
}
>=3.78.0runtime
next
Environment
{
  "local_name": "next",
  "alias": false,
  "peer_metadata": null
}
15.4.10development
tsup
Environment
{
  "local_name": "tsup",
  "alias": false,
  "peer_metadata": null
}
^8.3.7development
jsdom
Environment
{
  "local_name": "jsdom",
  "alias": false,
  "peer_metadata": null
}
26.1.0development
react
Environment
{
  "local_name": "react",
  "alias": false,
  "peer_metadata": null
}
^19.2.3development
eslint
Environment
{
  "local_name": "eslint",
  "alias": false,
  "peer_metadata": null
}
^9.39.1development
rimraf
Environment
{
  "local_name": "rimraf",
  "alias": false,
  "peer_metadata": null
}
^6.0.2development
shadcn
Environment
{
  "local_name": "shadcn",
  "alias": false,
  "peer_metadata": null
}
^3.6.3development
vitest
Environment
{
  "local_name": "vitest",
  "alias": false,
  "peer_metadata": null
}
^3.2.3development
publint
Environment
{
  "local_name": "publint",
  "alias": false,
  "peer_metadata": null
}
^0.3.16development
react-dom
Environment
{
  "local_name": "react-dom",
  "alias": false,
  "peer_metadata": null
}
^19.2.3development
@types/node
Environment
{
  "local_name": "@types/node",
  "alias": false,
  "peer_metadata": null
}
^22.15.3development
@types/react
Environment
{
  "local_name": "@types/react",
  "alias": false,
  "peer_metadata": null
}
19.2.3development
@xyflow/react
Environment
{
  "local_name": "@xyflow/react",
  "alias": false,
  "peer_metadata": null
}
^12.10.1development
@types/react-dom
Environment
{
  "local_name": "@types/react-dom",
  "alias": false,
  "peer_metadata": null
}
19.2.3development
@repo/eslint-config
Environment
{
  "local_name": "@repo/eslint-config",
  "alias": false,
  "peer_metadata": null
}
0.0.0development
@arethetypeswrong/cli
Environment
{
  "local_name": "@arethetypeswrong/cli",
  "alias": false,
  "peer_metadata": null
}
^0.17.3development
@mux/mux-player-react
Environment
{
  "local_name": "@mux/mux-player-react",
  "alias": false,
  "peer_metadata": null
}
^3.3.4development
@tanstack/react-query
Environment
{
  "local_name": "@tanstack/react-query",
  "alias": false,
  "peer_metadata": null
}
5.90.16development
@testing-library/react
Environment
{
  "local_name": "@testing-library/react",
  "alias": false,
  "peer_metadata": null
}
16.3.0development
@repo/typescript-config
Environment
{
  "local_name": "@repo/typescript-config",
  "alias": false,
  "peer_metadata": null
}
0.0.0development
@testing-library/jest-dom
Environment
{
  "local_name": "@testing-library/jest-dom",
  "alias": false,
  "peer_metadata": null
}
^6.9.1development
next
Environment
{
  "local_name": "next",
  "alias": false,
  "peer_metadata": null
}
>=14peer
react
Environment
{
  "local_name": "react",
  "alias": false,
  "peer_metadata": null
}
>=18peer
react-dom
Environment
{
  "local_name": "react-dom",
  "alias": false,
  "peer_metadata": null
}
>=18peer
@xyflow/react
Environment
{
  "local_name": "@xyflow/react",
  "alias": false,
  "peer_metadata": {
    "optional": true
  }
}
>=12peer
@mux/mux-player-react
Environment
{
  "local_name": "@mux/mux-player-react",
  "alias": false,
  "peer_metadata": {
    "optional": true
  }
}
>=3peer
@tanstack/react-query
Environment
{
  "local_name": "@tanstack/react-query",
  "alias": false,
  "peer_metadata": null
}
>=5peer

Distribution files

1 collected
Distribution 1

Source distribution

Source file
Checksums from source
{
  "shasum": "35ffb54acd29bd5bf5f73f28b416863e00949f0b",
  "tarball": "https://registry.npmjs.org/@01.software/sdk/-/sdk-0.4.3-dev.1774320821495.tgz",
  "fileCount": 74,
  "integrity": "sha512-SQMLVySzJE0SPivbBnzJJdixAG7PZ/5BtxqLhif8gQFlQC8aWSCkTE0erz4apvSGYuVNPtNlexbxQXRYzjTJpw==",
  "signatures": [
    {
      "sig": "MEYCIQD4wy6VvQna7GJbo9r5eOYaaX+angy0xlyR/1nSCzXsPwIhANGdnQhnFYRVOHIZgUcNQ3H2O514XEbN50ENVZV+Pu9h",
      "keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"
    }
  ],
  "unpackedSize": 1555558
}

Repository

PENDING
01-office/01-software

Shared repository data · GitHub

License detected in repository
Not collected

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 21:55 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package_version:714f9312dc4bdb42cf5e5c8b2fd07ef8f9e5b90b5e80c92af7ff97ff202b82bc

Other collected versions

0.65.113 Oct0.65.012 Oct0.56.125 Sept0.51.07 Sept0.49.012 Aug0.48.010 AugView all collected versions →