@01.software/sdk
Official TypeScript SDK for the 01.software platform.
Installation
npm install @01.software/sdk
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
import { createAnalytics } from '@01.software/sdk/analytics'
const analytics = createAnalytics({ publishableKey: 'pk_xxx' })
analytics.track('signup', { plan: 'pro', trial: false })
import { createClient } from '@01.software/sdk'
import { createServerClient } from '@01.software/sdk/server'
import {
handleWebhook,
createTypedWebhookHandler,
} from '@01.software/sdk/webhook'
import { createRealtimeClient } from '@01.software/sdk/realtime'
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'
Getting Started
Client
import { createClient } from '@01.software/sdk'
const client = createClient({
publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY,
})
const { docs } = await client.collections.from('products').find({
limit: 10,
where: { status: { equals: 'published' } },
})
Server Client
import { createServerClient } from '@01.software/sdk/server'
const server = createServerClient({
publishableKey: process.env.SOFTWARE_PUBLISHABLE_KEY,
secretKey: process.env.SOFTWARE_SECRET_KEY,
})
const order = await server.commerce.orders.create({
orderNumber: generateOrderNumber(),
customerSnapshot: { email: 'user@example.com' },
shippingAddress: { recipientName: 'John', phone: '010-1234-5678', postalCode: '12345', address1: 'Seoul', address2: 'Apt 101' },
orderItems: [...],
totalAmount: 10000,
pgPaymentId: 'pay_123',
discountCode: 'WELCOME10',
})
await server.query.prefetchQuery({
collection: 'products',
options: { limit: 10 },
})
API
Client Configuration
const client = createClient({
publishableKey: string,
})
const server = createServerClient({
publishableKey: string,
secretKey: string,
})
publishableKey | string | API publishable key |
secretKey | string | API secret key (server only) |
API URL은 환경변수로 오버라이드 가능합니다:
SOFTWARE_API_URL (서버용) 또는 NEXT_PUBLIC_SOFTWARE_API_URL (브라우저용)
- 미설정 시: dev 빌드(
@dev 태그)는 api-dev.01.software, 정식 릴리즈는 api.01.software
Query Builder
Access collections via client.collections.from(slug).
Note: client.collections.from() returns a ReadOnlyQueryBuilder (only find, findById, count, findMetadata, findMetadataById). Write operations (create, update, remove, updateMany, removeMany) are only available on server.collections.from().
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 },
})
const { docs } = await client.collections.from('products').find({
select: { title: true, slug: true, price: true, thumbnail: true },
joins: false,
})
const product = await client.collections.from('products').findById(id, {
populate: { brands: { name: true, logo: true } },
joins: { variants: { limit: 50 } },
})
const product = await client.collections.from('products').findById('id')
const { doc, message } = await server.collections
.from('products')
.create({ name: 'Product' })
const { doc } = await server.collections
.from('images')
.create({ alt: 'Hero image' }, { file: imageFile, filename: 'hero.jpg' })
const { doc } = await server.collections
.from('products')
.update('id', { name: 'Updated' })
await server.collections
.from('images')
.update('id', { alt: 'New alt' }, { file: newFile })
const deletedDoc = await server.collections.from('products').remove('id')
const { totalDocs } = await client.collections.from('products').count()
const metadata = await client.collections
.from('products')
.findMetadata(
{ where: { slug: { equals: 'my-product' } } },
{ siteName: 'My Store' },
)
const metadataById = await client.collections
.from('products')
.findMetadataById('id', {
siteName: 'My Store',
})
await server.collections.from('products').updateMany(where, data)
await server.collections.from('products').removeMany(where)
API Response Types (Payload Native)
The SDK returns Payload CMS native response types without wrapping:
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
}
interface PayloadMutationResponse<T> {
message: string
doc: T
errors?: unknown[]
}
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 } |
findMetadata() | Metadata | null - Next.js Metadata (null if no match) |
findMetadataById() | Metadata - Next.js Metadata (throws on 404) |
React Query Hooks
Read hooks are available on both Client and ServerClient via client.query.*. Mutation hooks (useCreate, useUpdate, useRemove) are only available on ServerClient.
const { data, isLoading } = client.query.useQuery({
collection: 'products',
options: { limit: 10 },
})
const { data } = client.query.useSuspenseQuery({
collection: 'products',
options: { limit: 10 },
})
const { data } = client.query.useQueryById({
collection: 'products',
id: 'product_id',
})
const { data, fetchNextPage, hasNextPage } = client.query.useInfiniteQuery({
collection: 'products',
options: { limit: 20 },
})
const { mutate: create } = client.query.useCreate({ collection: 'images' })
const { mutate: update } = client.query.useUpdate({ collection: 'products' })
const { mutate: remove } = client.query.useRemove({ collection: 'products' })
create({ data: { alt: 'Hero' }, file: imageFile, filename: 'hero.jpg' })
update({ id: 'product_id', data: { title: 'Updated' } })
remove('product_id')
await client.query.prefetchQuery({
collection: 'products',
options: { limit: 10 },
})
await client.query.prefetchQueryById({
collection: 'products',
id: 'product_id',
})
await client.query.prefetchInfiniteQuery({
collection: 'products',
pageSize: 20,
})
client.query.invalidateQueries('products')
client.query.getQueryData('products', 'list', options)
client.query.setQueryData('products', 'detail', id, data)
const { data: profile } = client.query.useCustomerMe()
const { mutate: login } = client.query.useCustomerLogin()
const { mutate: register } = client.query.useCustomerRegister()
const { mutate: logout } = client.query.useCustomerLogout()
login({ email: 'user@example.com', password: 'password' })
client.query.useCustomerForgotPassword()
client.query.useCustomerResetPassword()
client.query.useCustomerChangePassword()
client.query.invalidateCustomerQueries()
client.query.getCustomerData()
client.query.setCustomerData(profile)
Customer Auth
Available on Client via client.customer.auth.*.
const client = createClient({
publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY,
customer: { persist: true },
})
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',
})
const profile = await client.customer.auth.me()
client.customer.auth.isAuthenticated()
client.customer.auth.logout()
const orders = await client.commerce.orders.listMine({
page: 1,
limit: 10,
status: 'paid',
})
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.
await server.commerce.orders.create(params)
await server.commerce.orders.update({ orderNumber, status })
await server.commerce.orders.checkout({ cartId, pgPaymentId?, orderNumber, customerSnapshot, discountCode? })
const { docs: [order] } = await server.collections.from('orders').find({
where: { orderNumber: { equals: 'ORD-...' } },
limit: 1,
depth: 1,
})
await server.commerce.orders.createFulfillment({ orderNumber, carrier, trackingNumber, items })
await server.commerce.orders.bulkImportFulfillments({ items: [{ orderNumber, carrier?, trackingNumber? }] })
await server.commerce.orders.updateTransaction({ pgPaymentId, status, paymentMethod, receiptUrl })
await server.commerce.orders.createReturn({ orderNumber, returnItems, refundAmount, reason? })
await server.commerce.orders.updateReturn({ returnId, status })
await server.commerce.orders.returnWithRefund({ orderNumber, returnItems, refundAmount, 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? })
Commerce Product
Available on both Client and ServerClient via commerce.product.*.
const { results, allAvailable } = await client.commerce.product.stockCheck({
items: [{ variantId: '...', quantity: 2 }],
})
Commerce Cart
Available on both Client and ServerClient via commerce.cart.*.
await client.commerce.cart.addItem({
cartId,
product,
variant,
option,
quantity,
})
await client.commerce.cart.updateItem({ cartItemId, quantity })
await client.commerce.cart.removeItem({ cartItemId })
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 })
Available only on ServerClient via server.community.moderation.*.
await server.community.moderation.banCustomer({ customerId, isPermanent?, bannedUntil?, reason? })
await server.community.moderation.unbanCustomer({ customerId })
Webhook
import {
handleWebhook,
createCustomerAuthWebhookHandler,
createTypedWebhookHandler,
} from '@01.software/sdk'
export async function POST(request: Request) {
return handleWebhook(request, async (event) => {
console.log(event.collection, event.operation, event.data)
})
}
export async function POST(request: Request) {
return handleWebhook(request, handler, {
secret: process.env.WEBHOOK_SECRET,
})
}
const handler = createTypedWebhookHandler('orders', async (event) => {
console.log(event.data.orderNumber)
})
const customerAuthHandler = createCustomerAuthWebhookHandler({
passwordReset: async ({ email, resetPasswordToken }) => {
await sendPasswordResetEmail(email, resetPasswordToken)
},
})
Supported Collections
Source of truth: packages/sdk/src/core/collection/const.ts (COLLECTIONS: 75).
| Tenant | tenants, tenant-metadata, tenant-logos |
| Products | products, product-variants, product-options, product-option-values, product-categories, product-tags, product-collections, brands, brand-logos |
| Orders | orders, order-items, returns, return-items, fulfillments, fulfillment-items, transactions |
| Customers | customers, customer-profiles, customer-profile-lists, customer-addresses |
| Carts | carts, cart-items |
| Commerce | discounts, promotions, shipping-policies |
| Content | documents, document-categories, document-types, articles, article-authors, article-categories, article-tags, links, link-categories, link-tags |
| Playlists / Tracks | playlists, playlist-categories, playlist-tags, tracks, track-categories, track-tags |
| Galleries | galleries, gallery-categories, gallery-tags, gallery-items |
| Canvas | canvases, canvas-node-types, canvas-edge-types, canvas-categories, canvas-tags, canvas-nodes, canvas-edges |
| Videos | videos, video-categories, video-tags |
| Live Streams | live-streams |
| Media | images |
| Forms | forms, form-submissions |
| Community | posts, comments, reactions, reaction-types, bookmarks, post-categories, reports, community-bans |
| Events | event-calendars, events, event-categories, event-occurrences, event-tags |
Server-only collections: customer-groups is available from createServerClient().collections for segmentation and campaign targeting, but is 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)) ?? []
Note: Prefer resolveRelation. It covers the same normalization use case directly.
generateOrderNumber
import { generateOrderNumber } from '@01.software/sdk'
const orderNumber = generateOrderNumber()
formatOrderName
import { formatOrderName } from '@01.software/sdk'
formatOrderName([{ product: { title: 'Product A' } }])
formatOrderName([
{ product: { title: 'Product A' } },
{ product: { title: 'Product B' } },
])
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'
<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} />,
}}
/>
<RichTextContent data={content} nodeMap={richTextNodeMap} />
<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_...
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 (
swatchColor, 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.
Migration Guide
v0.16.0 (Phase 1–7 sync — additive)
New error codes propagated via SDKError.code (no breaking change; existing callers ignore unknown codes safely):
account_suspended | P1 | Suspended session / sk01_ / pat01_ / customer JWT — 401 |
pat_tenant_header_forbidden | P1 | pat01_ request carrying any X-Tenant-Id header — 401 |
tenant_mismatch | P3 | Cross-tenant FK rejection (forms / community / orders) |
server_derived | P3 | Body-driven write into a server-derived state field — 422 |
scope_denied | P5 | pat01_ 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:
| Customers | socialId | providerUserId |
| Customers | loginAttempts | loginAttemptCount |
| Customers | resetPasswordExpiry | resetPasswordExpiresAt |
| Orders, Carts | shippingFee | shippingAmount |
| Carts | itemsTotal | subtotalAmount |
| Transactions | paymentId | pgPaymentId |
| Discounts | type | discountType |
| Discounts | value | discountValue |
| Discounts | usageLimit | maxUses |
| Discounts | usageCount | usesCount |
| Discounts | perCustomerLimit | maxUsesPerCustomer |
| ShippingPolicies | baseFee | baseAmount |
| ShippingPolicies | freeShippingThreshold | freeShippingMinAmount |
| Documents | effectiveDate | effectiveAt |
| Documents | expiryDate | expiresAt |
| Articles | readTime | readingMinutes |
| ApiUsage | count | apiCallCount |
| ApiUsage | storageUsed | storageUsedBytes |
| ApiUsage | totalDocuments | documentCount |
Collection renames:
order-products → order-items
return-products → return-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