| name | pallastrade-typescript-sdk |
| description | Use when the user is building a TypeScript or JavaScript client against PallasTrade — a Next.js storefront, a custom admin tool, a webhook receiver, a backend service that talks to the PallasTrade API. Covers @pallastrade/sdk (Store API) and @pallastrade/admin-sdk (Admin API). Common phrasings include "PallasTrade SDK", "createClient", "publishable key", "store client", "admin client", "TypeScript types from PallasTrade", "Zod schemas", "verifyWebhookSignature", "retry config", "MSW PallasTrade", "@pallastrade/sdk", "@pallastrade/admin-sdk". For curl/raw HTTP usage and protocol details, see pallastrade-api-v3. |
PallasTrade TypeScript SDKs
Two npm packages, one shared HTTP core:
| Package | Surface | Auth | Status |
|---|
@pallastrade/sdk | Store API (/api/v3/store/*) | Publishable key + optional JWT customer | Stable (1.x) |
@pallastrade/admin-sdk | Admin API (/api/v3/admin/*) | Secret key OR JWT admin | Developer Preview (next dist-tag) |
@pallastrade/sdk-core | Shared HTTP, retry, error layer | n/a | Internal (not for direct use) |
Both packages publish:
dist/index.js — flat client (createClient / createAdminClient)
dist/types/ — generated TypeScript types from Alba serializers
@pallastrade/sdk also publishes dist/zod/ — runtime validation schemas + dist/webhooks.js — signature verifier
@pallastrade/sdk — Store API client
Install
npm install @pallastrade/sdk
Quickstart
import { createClient } from '@pallastrade/sdk'
const client = createClient({
baseUrl: 'https://my-pallastrade.example.com',
publishableKey: 'pk_CzEKBTWFiuNLgz4wciLsS59n',
locale: 'en-US',
currency: 'USD',
country: 'US',
channel: 'online',
})
const { data: products, meta } = await client.products.list({ limit: 20 })
const product = await client.products.get('prod_86Rf07xd4z')
const cart = await client.carts.create()
await client.carts.items.create(cart.id, { variant_id: 'variant_…', quantity: 1 }, {
guestToken: cart.token,
})
Resource shape
Resources follow one method vocabulary:
client.<resource>.list(params?, options?)
client.<resource>.get(idOrSlug, params?, options?)
client.<resource>.create(body, options?)
client.<resource>.update(id, body, options?)
client.<resource>.delete(id, options?)
Full five-method CRUD is an Admin SDK property. On the Store SDK most resources are read-only or partial — products exposes list/get/filters plus reviews (products.reviews.list(productId) — approved, public; products.reviews.create(productId, { rating, title?, body? }, { token }) — signed-in customer submits a pending review, P0-4); categories, countries, orders, policies, posts, markets, currencies, locales are read-only; customers exposes only create. Store writes are limited to carts, wishlists, the customer's own account/addresses, product reviews, and back-in-stock subscriptions — client.backInStockSubscriptions.create(productId, { email }) (guest-accessible, POST /products/:id/back_in_stock_subscriptions); catalog writes require the Admin SDK. If a method doesn't typecheck, it doesn't exist on that surface — don't force it.
client.posts is the CMS blog resource: posts.list(params?, options?) returns a paginated Post list (published only, newest first) and posts.get(slugOrId, options?) returns a single Post. The Post type and PostSchema are exported from @pallastrade/sdk (generated by Typelizer from PostSerializer).
Method name is always get, never show. The delete method is delete, not destroy. Nested resources (e.g. client.carts.items.create(cartId, params, options)) take the parent prefixed ID as the first positional argument.
Standard e-commerce flow extensions (P1 2026-08-30, PRD-20260829-checkout)
New Cart entity (pallastrade_carts) plus order-domain payments:
client.carts.submit(cartId, options?) — POST /carts/:id/submit; converts the cart into an Order (or_-prefixed) and returns it (Order type now carries state, status, submitted_at, cart_id, payment_methods).
client.orders.paymentSessions.create(orderId, { payment_method_id, amount?, external_data? }, options?) / .get / .complete(orderId, sessionId, { session_result?, external_data? }, options?) — order-scoped payment sessions (Stripe Checkout client_secret etc.).
client.shippingMethods.list(options?) — GET /shipping_methods; DeliveryMethod carries display_estimated_price for the order-confirmation radio list.
- Cart line items accept
selected?: boolean (UpdateLineItemParams / UpdateCartItemParams) — only selected items are submitted to the order.
Customer auth (JWT)
After login, attach the JWT per request via options.token. There is no setAccessToken — the SDK doesn't hold customer tokens in client state.
const { token, refresh_token, user } = await client.auth.login({ email, password })
const orders = await client.customer.orders.list({}, { token })
The login response's token field is the customer JWT. Store and pass per-request — server-rendered apps can stash it in a session cookie; client-side apps store it in memory and refresh via client.auth.refresh({ refresh_token }).
Setting defaults dynamically
client.setLocale('fr')
client.setCurrency('EUR')
client.setCountry('FR')
client.setChannel('wholesale')
Each setter mutates the in-memory defaults. The next request uses the new values; concurrent in-flight requests use whatever was set when their headers were built.
List params (Ransack)
await client.products.list({
page: 2,
limit: 50,
name_cont: 'shirt',
price_gte: 20,
price_lte: 100,
sort: '-created_at',
expand: ['images', 'default_variant'],
})
List params are flat. Internally any key that is not page/limit/expand/sort/fields becomes a Ransack predicate via transformListParams in @pallastrade/sdk-core: name_cont: 'shirt' → q[name_cont]=shirt (array values get a [] suffix: q[with_option_value_ids][]). The expand array becomes ?expand=images,default_variant. Do not nest predicates under a filter key — there is no such param; a nested object would serialize as q[filter]=[object Object].
Error handling
import { PallasTradeError } from '@pallastrade/sdk'
try {
await client.carts.items.create(cartId, { variant_id, quantity: 1 }, { guestToken })
} catch (err) {
if (err instanceof PallasTradeError) {
err.status
err.code
err.message
err.details
}
throw err
}
For 422 in form contexts, err.details keys are attribute names, so they map directly onto form-library error setters (e.g. React Hook Form's setError); base errors are non-field-specific — render them as a form-level banner.
Retry config
const client = createClient({
baseUrl, publishableKey,
retry: { maxRetries: 3, baseDelay: 200, retryOnStatus: [429, 502, 503, 504] },
})
const client = createClient({ baseUrl, publishableKey, retry: false })
Defaults: 2 retries, exponential backoff with jitter (300ms base, capped at 10s), retries on 429/500/502/503/504 + network errors. Honors Retry-After headers. The 5xx statuses only retry for idempotent requests — GET/HEAD, or any request carrying an Idempotency-Key; since mutating requests get an auto-generated Idempotency-Key when retries are on, they're covered too. Non-keyed mutations retry on 429 only.
Custom fetch (testing, server-only, etc.)
const client = createClient({
baseUrl, publishableKey,
fetch: customFetch,
})
Use this for:
- Server-only fetch (Next.js server components:
fetch from next/cache)
- Mocking in tests (pass MSW's
fetch or a stub)
- Adding tracing headers (wrap
fetch with an OpenTelemetry instrument)
Generated types
import type { Product, Order, Cart } from '@pallastrade/sdk/types'
function renderProduct(product: Product) { ... }
Types are generated from Alba serializers via bundle exec rake typelizer:generate and published with each release. They always match the API exactly (no drift).
Zod schemas (runtime validation)
import { ProductSchema } from '@pallastrade/sdk/zod'
const product = ProductSchema.parse(unsafeData)
const parsed = ProductSchema.safeParse(unsafeData)
if (parsed.success) { ... }
Use when consuming API responses you don't fully trust (cached payloads, webhook bodies, third-party proxies). Generated from the TypeScript types via pnpm generate:zod.
Webhook signature verification
import { verifyWebhookSignature } from '@pallastrade/sdk/webhooks'
const rawBody = await request.text()
const signature = request.headers.get('x-pallastrade-webhook-signature')!
const timestamp = request.headers.get('x-pallastrade-webhook-timestamp')!
const isValid = verifyWebhookSignature(
rawBody,
signature,
timestamp,
process.env.PALLASTRADE_WEBHOOK_SECRET!,
300,
)
if (!isValid) return new Response('Unauthorized', { status: 401 })
const event = JSON.parse(rawBody)
This is a Node-only export (uses node:crypto). For Edge runtimes, use Web Crypto manually — see pallastrade-events-webhooks skill.
Typed webhook event payloads
import type { WebhookEvent } from '@pallastrade/sdk/webhooks'
import type { Order } from '@pallastrade/sdk'
const event = JSON.parse(rawBody) as WebhookEvent<Order>
if (event.name === 'order.completed') {
event.data.id
event.data.total
}
@pallastrade/admin-sdk — Admin API client
Install
npm install @pallastrade/admin-sdk@next
The Admin SDK is in Developer Preview and published under the next dist-tag — a plain npm install @pallastrade/admin-sdk resolves latest and will not get the preview release.
Two auth modes
Mode 1: secret key (server-to-server apps)
import { createAdminClient } from '@pallastrade/admin-sdk'
const admin = createAdminClient({
baseUrl: 'https://my-pallastrade.example.com',
secretKey: process.env.PALLASTRADE_ADMIN_SECRET_KEY!,
storeId: 'store_k5nR8xLq',
})
const { data: orders } = await admin.orders.list({ status_eq: 'placed' })
The secret key carries scopes (read_orders, write_products, etc.) — see pallastrade-api-v3 for the scope list. Requests for endpoints outside the key's scopes get 403.
Mode 2: JWT (human admin users)
const admin = createAdminClient({
baseUrl,
jwtToken: jwtFromLogin,
storeId: currentStoreId,
})
JWT mode uses CanCanCan abilities on the backend. What the user can do depends on their role. The Admin SDK defaults to credentials: 'include' so the admin refresh-token cookie is sent on /api/v3/admin/auth/* endpoints.
Unlike the Store SDK, the admin client holds its JWT in client state: call admin.setToken(newJwt) after a refresh, admin.setStore(storeId) to switch stores (sets the X-PallasTrade-Store-Id header), and admin.onUnauthorized(async () => { /* refresh token, call admin.setToken(...) */ return true }) to transparently retry a 401'd request once with the new token (paths under /auth/ are excluded from retry; return false to let the 401 propagate).
Resource shape
Same five-method shape as the Store SDK, full CRUD enabled by default for every resource:
admin.products.list()
admin.products.get('prod_…')
admin.products.create({ name, description, ... })
admin.products.update('prod_…', { name: 'Renamed' })
admin.products.delete('prod_…')
Singletons use get, not show
admin.me.get()
admin.store.get()
(Never show — the convention is uniform.)
Exports endpoint
For example, a CSV export flow:
const exportRecord = await admin.exports.create({
type: 'PallasTrade::Exports::Orders',
search_params: { status_eq: 'placed' },
})
Always stream bytes (send_data backend / Blob frontend); never redirect_to attachment.url + window.location.href. JWT downloads must use fetch + Blob so the Authorization header is sent.
@pallastrade/sdk-core — shared layer (internal)
Private package providing:
createRequestFn(config, basePath, auth, defaults) — the HTTP function used by both clients
PallasTradeError class
resolveRetryConfig + exponential backoff
transformListParams (Ransack predicate transform)
Don't import directly. The public @pallastrade/sdk and @pallastrade/admin-sdk re-export everything you need.
Extending the SDK — custom endpoints, custom resources
PallasTrade is a self-hosted open-source platform. Most real projects add custom API endpoints (vendor models, B2B quote flows, loyalty programs, integrations specific to the merchant's business). The SDK is built to wrap these the same way it wraps the built-in resources — you're not stuck with what ships in the box.
There are three escape hatches, in order of increasing investment:
1. One-off custom calls — client.request
Both Client and AdminClient expose a low-level request<T>(method, path, options?) that uses the same auth, retry, and base URL as the built-in resources. Paths are relative to /api/v3/store (Store SDK) or /api/v3/admin (Admin SDK).
type Brand = { id: string; name: string; slug: string }
type ListResponse<T> = { data: T[]; meta: { page: number; pages: number; count: number } }
const { data: brands } = await client.request<ListResponse<Brand>>('GET', '/brands', {
params: { 'q[name_cont]': 'acme', page: 1, limit: 25 },
})
const brand = await client.request<Brand>('GET', `/brands/${id}`)
const created = await client.request<Brand>('POST', '/brands', { body: { name: 'New brand' } })
const vendor = await admin.request<Vendor>('POST', '/vendors', {
body: { name, contact_email, commission_rate: 0.15 },
})
request is what every built-in resource is built on top of — there's no "private" version. Reach for it whenever:
- You shipped a custom endpoint via the
pallastrade:api_resource generator (see pallastrade-resource skill) and want to call it from TypeScript without writing a wrapper class.
- You need to call an endpoint added by a third-party PallasTrade extension.
- You're prototyping and the wrapper isn't worth it yet.
2. Wrapped custom resource — for code you reuse
Once you're calling a custom endpoint from more than one place, wrap it. The convention matches what ships in the SDK: a class that takes the request function in its constructor and exposes the five-method shape.
import type { RequestFn, ListResponse } from '@pallastrade/sdk'
export interface Brand {
id: string
name: string
slug: string
description: string | null
}
export interface BrandListParams {
page?: number
limit?: number
name_cont?: string
slug_eq?: string
}
export class BrandsClient {
constructor(private readonly request: RequestFn) {}
list(params: BrandListParams = {}) {
const { page, limit, ...predicates } = params
const q = Object.fromEntries(Object.entries(predicates).( [, v]))
.<<>>(, , {
: { page, limit, ...q },
})
}
() {
.<>(, )
}
() {
.<>(, , { body })
}
() {
.<>(, , { body })
}
() {
.<>(, )
}
}
3. Extending the client itself
Attach the wrapper to the client so consumer code reads the same as built-in resources (pallastrade.brands.list() not new BrandsClient(pallastrade.request).list()):
import { createClient, type Client, type ClientConfig } from '@pallastrade/sdk'
import { BrandsClient } from './brands-client'
import { VendorsClient } from './vendors-client'
export interface ExtendedClient extends Client {
brands: BrandsClient
vendors: VendorsClient
}
export function createExtendedClient(config: ClientConfig): ExtendedClient {
const client = createClient(config) as ExtendedClient
client.brands = new BrandsClient(client.request)
client.vendors = new VendorsClient(client.request)
return client
}
import { createExtendedClient } from '@/pallastrade-extensions'
const pallastrade = createExtendedClient({ baseUrl, publishableKey })
const { data: brands } = await pallastrade.brands.list({ name_cont: 'acme' })
Same trick for @pallastrade/admin-sdk: import createAdminClient + AdminClient, extend the interface, attach wrappers in your factory.
Decorating a built-in resource
If a third-party extension adds endpoints under an existing resource (e.g. /products/:id/republish for a syndication extension), decorate the existing resource instead of replacing it. Don't spread the client itself — createClient returns an object whose resources live on its prototype, so { ...base } would copy only the locale/currency setters and drop carts, request, and every other resource. Attach the decorated resource onto the client instead:
import { createClient } from '@pallastrade/sdk'
const base = createClient({ baseUrl, publishableKey })
export const pallastrade = Object.assign(base, {
products: {
...base.products,
republish(id: string) {
return base.request<void>('POST', `/products/${id}/republish`)
},
},
})
Capturing Object.assign's return value gives pallastrade the intersection type, so pallastrade.products.republish(...) typechecks alongside the built-in pallastrade.products.list(). Spreading base.products is safe — resource methods are own properties with lexically bound this, so they keep working after the copy.
Type generation for custom resources
If your custom endpoint uses Alba serializers (which it does if you generated it with pallastrade:api_resource), you can plug into the same Typelizer pipeline that produces the official SDK types:
cd pallastrade/api && bundle exec rake typelizer:generate
The output goes to packages/sdk/src/types/generated/ if you're in the monorepo, but in a standalone consumer project you'd configure Typelizer's output path to your project's types/pallastrade-extensions/ directory. Re-run after serializer changes; check the generated .d.ts into version control.
For Zod schemas to match, copy the same pnpm generate:zod pattern — it's a small custom script (packages/sdk/scripts/generate-zod.ts) that converts the generated TS interfaces to Zod schemas; point its input/output dirs at your generated types directory.
If you're not running the monorepo, just hand-write the TypeScript interfaces. The serializer is the source of truth either way — match attribute names + types and you're done.
What you should NOT do
- Don't fork
@pallastrade/sdk. When the package updates, you're stuck merging. Extend, don't fork.
- Don't depend on
@pallastrade/sdk-core — it's a private workspace package, never published to npm. Everything you need is re-exported from the public packages: @pallastrade/sdk exports RequestFn, ListResponse, RequestOptions, and PallasTradeError; @pallastrade/admin-sdk exports ListResponse, RequestOptions, and PallasTradeError (no RequestFn — type admin wrapper constructors with AdminClient['request']). The HTTP and retry internals can change between minor releases.
- Don't bypass the SDK by calling
fetch directly in app code. You lose retry, error normalization, default headers (locale/currency/channel), and auth. If the SDK doesn't cover what you need, use client.request — it's the same fetch with all the goodies still applied.
Testing with MSW
Both SDKs work great with Mock Service Worker. The SDK doesn't intercept fetch — it just calls fetch — so MSW handlers see the requests and respond with whatever you want.
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'
export const server = setupServer(
http.get('https://test.pallastrade.local/api/v3/store/products', () => {
return HttpResponse.json({
data: [{ id: 'prod_test1', name: 'Test product' }],
meta: { page: 1, count: 1, pages: 1, limit: 25 },
})
}),
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
import { createClient } from '@pallastrade/sdk'
test('lists products', async () => {
const client = createClient({
baseUrl: 'https://test.pallastrade.local',
: ,
})
{ data } = client..()
(data).()
})
Type regeneration pipeline
After backend serializer changes, the types and Zod schemas need to be regenerated:
cd pallastrade/api && bundle exec rake typelizer:generate
cd packages/sdk && pnpm generate:zod
cd packages/sdk && pnpm test
In the monorepo, a Lefthook pre-commit hook runs steps 1–2 automatically whenever backend/pallastrade_gems/pallastrade_api/app/serializers/**/*.rb files are committed.
If you're a SDK consumer (not the maintainer), you don't run this — just npm update @pallastrade/sdk to get the latest types.
Common patterns
Next.js App Router server component
import { createClient } from '@pallastrade/sdk'
const pallastrade = createClient({
baseUrl: process.env.PALLASTRADE_API_URL!,
publishableKey: process.env.PALLASTRADE_PUBLISHABLE_KEY!,
fetch,
})
export default async function ProductsPage() {
const { data: products } = await pallastrade.products.list({ limit: 24 })
return <ProductGrid products={products} />
}
Webhook receiver (Next.js API route)
import { verifyWebhookSignature, type WebhookEvent } from '@pallastrade/sdk/webhooks'
import type { Order } from '@pallastrade/sdk/types'
export async function POST(request: Request) {
const rawBody = await request.text()
const signature = request.headers.get('x-pallastrade-webhook-signature') ?? ''
const timestamp = request.headers.get('x-pallastrade-webhook-timestamp') ?? ''
if (!verifyWebhookSignature(rawBody, signature, timestamp, process.env.PALLASTRADE_WEBHOOK_SECRET!)) {
return new Response('Unauthorized', { status: 401 })
}
const event = JSON.parse(rawBody) as WebhookEvent<Order>
switch (event.name) {
case :
(event.)
}
.({ : })
}
React Query integration
import { useQuery, useMutation } from '@tanstack/react-query'
import { pallastrade } from '@/lib/pallastrade-client'
function useProducts(filters: ProductFilters) {
return useQuery({
queryKey: ['products', filters],
queryFn: () => pallastrade.products.list(filters),
})
}
function useCreateCart() {
return useMutation({
mutationFn: () => pallastrade.carts.create(),
})
}
Common pitfalls
"Why is currency not sticking?"
setCurrency mutates an in-memory default. If you have multiple clients (server-rendered + client-side rehydrated), each has its own defaults. Either set on both or pass per-request via client.products.list({ currency: 'EUR' }).
"Signature verification fails on Next.js Edge"
@pallastrade/sdk/webhooks uses node:crypto (Node only). For Edge runtimes, write the HMAC verify by hand against Web Crypto's subtle.importKey + subtle.sign. See pallastrade-events-webhooks for the algorithm.
"Types changed, my code broke"
@pallastrade/sdk follows semver. Minor versions can add fields (your code keeps working). Major versions can break (read the changelog before bumping). Pin to a specific minor if you want zero surprise.
"Where's the OpenAPI?"
node_modules/@pallastrade/docs/dist/api-reference/store.yaml (Store) — generated from the Rails integration specs via Rswag. Authoritative reference for everything the SDK exposes.
Where to read further
- Store SDK source:
packages/sdk/src/store-client.ts — every resource and its methods.
- Admin SDK source status: the Developer Preview package is described by this Skill, but the former
admin-sdk package directory is not present under platform/packages in the current repository checkout. Do not invent or edit a local source path; verify the released package/current integration branch before proposing source changes.
- API protocol details: see the
pallastrade-api-v3 skill — auth, prefixed IDs, pagination, envelope.
- Webhooks delivery side: see the
pallastrade-events-webhooks skill — endpoint config, retry logic, payload shape.