Loading...
Loading...
Use when the user is building a TypeScript or JavaScript client against Spree 6 — a Next.js storefront, a mobile/web app, an integration or back-office tool, a webhook receiver. Covers @spree/sdk (Store API), @spree/admin-sdk (Admin API) and points to @spree/seller-sdk (marketplace). Common phrasings include "Spree SDK", "createClient", "createAdminClient", "store client", "admin client", "cart token / spreeToken", "carts.complete", "orders.get after checkout", "TypeScript types from Spree", "Zod schemas", "verifyWebhookSignature", "retry config", "client.request custom endpoint", "MSW Spree", "@spree/sdk", "@spree/admin-sdk". For curl/raw HTTP and protocol details, see spree-api-v3.
npx skill4agent add spree/agent-skills spree-typescript-sdk| Package | Surface | Auth | Version |
|---|---|---|---|
| Store API ( | Publishable key + optional customer JWT / guest token | 2.x (currently 2.0 beta) |
| Admin API ( | Secret key or staff JWT | 1.x beta |
| Seller API ( | Seller JWT + seller ID ( | 1.x beta. Singletons use |
| Shared HTTP, retry, errors | — | Internal, never published. Don't import it |
SpreeErrorrequest@spree/sdkbetapnpm add @spree/sdk@beta@spree/clicreate-spree-app@spree/sdk@spree/sdk/types@spree/sdk/zod@spree/sdk/webhooks/types1.0.0-beta.3/zod@spree/admin-sdk/zod@spree/seller-sdk/zod<Type>Schemazodimport { OrderSchema } from '@spree/admin-sdk/zod'
const order = OrderSchema.parse(await adminClient.orders.get('or_xxx')) // throws on a shape mismatchimport { createClient } from '@spree/sdk'
const client = createClient({
baseUrl: 'https://shop.example.com',
publishableKey: 'pk_xxx',
// optional defaults, sent as X-Spree-* headers
country: 'DE', locale: 'de', currency: 'EUR', channel: 'online',
})
const { data: products, meta } = await client.products.list({ name_cont: 'shirt', limit: 24, expand: ['default_variant'] })
const product = await client.products.get('classic-tee') // id or slugclient.setLocale('fr')setCurrencysetCountrysetChannellist(params?, options?)get(idOrSlug, params?, options?)create(body, options?)update(id, body, options?)delete(id, options?)getshowdeletedestroyclient.carts.items.create(cartId, body, options)| Resource | What you get |
|---|---|
| Catalog reads ( |
| Context and reference data |
| Cart and checkout |
| Completed orders |
| Accounts |
| Wishlists |
| Double opt-in newsletter |
| B2B self-service (see |
/customers/me/data_requeststax_identifierpo_documentclient.request// Customer login: tokens aren't stored on the client; pass them per request
const { token, refresh_token, user } = await client.auth.login({ email, password })
const { data: orders } = await client.customer.orders.list({}, { token })
const fresh = await client.auth.refresh({ refresh_token }) // rotates both tokens
await client.auth.logout({ refresh_token })
// Third-party IdP (strategy registered on the server)
await client.auth.login({ provider: 'external_idp', token: idpJwt })RequestOptionstokenspreeTokenX-Spree-TokenidempotencyKeyheaderslet cart = await client.carts.create()
const opts = { spreeToken: cart.token } // or { token } for a signed-in customer
cart = await client.carts.items.create(cart.id, { variant_id: 'variant_xxx', quantity: 1 }, opts)
cart = await client.carts.update(cart.id, { email, shipping_address, use_shipping: true }, opts)
// cart.requirements → [{ step, field, code, message }] (what still blocks completion)
const result = await client.carts.complete(cart.id, opts) // Order (or_…, order.cart_id === cart.id) | OrderGroup (ogrp_…)client.carts.get(cartId)SpreeErrorcart_not_foundclient.orders.get(cartId, {}, opts)cart.requirementsspree-checkoutOrdernumbercart_idcoupon_codepayment_statusfulfillment_statusdisplay_*statestatuscarts.completePromise<Order | OrderGroup>ogrp_…orders[]isOrderGroup@spree/sdkconst orders = isOrderGroup(result) ? result.orders : [result]Order@spree/admin-sdkisOrderGroupstring | null"29.99"display_*nullstring | nullparseFloatawait client.products.list({ page: 2, limit: 50, name_cont: 'shirt', sort: '-created_at', expand: ['media'], fields: ['name', 'slug'] })pagelimitsortexpandfieldsq[...]name_contq[name_cont][]filterimport { SpreeError } from '@spree/sdk'
try { await client.carts.items.create(cartId, { variant_id, quantity: 1 }, opts) }
catch (err) {
if (err instanceof SpreeError) {
err.status // 422
err.code // 'insufficient_stock' | 'validation_error' | ...
err.details // { quantity: ['...'] } (attribute keys; 'base' = form-level)
}
throw err
}Retry-AfterIdempotency-Keyretry: { maxRetries, baseDelay, maxDelay, retryOnStatus, retryOnNetworkError }retry: falsefetchimport { createAdminClient } from '@spree/admin-sdk'
// Integration (server only)
const admin = createAdminClient({ baseUrl, secretKey: process.env.SPREE_SECRET_KEY! })
const { data: orders } = await admin.orders.list({ status_eq: 'placed', sort: '-completed_at' })
const order = await admin.orders.get('or_xxx', { expand: ['items', 'payments', 'fulfillments'] })
await admin.orders.cancel('or_xxx', { cancel_reason_id: 'ocr_xxx', cancel_note: 'Customer request' })
await admin.orders.refunds.create('or_xxx', { payment_id: 'py_xxx', amount: '25.00', refund_reason_id: 'rr_xxx' }) // refund_reason_id, not reason_iddetails.required_scopespree-api-v3/references/scopes.mdadmin.auth.login({ email, password }){ provider, ... }{ token, user }admin.setToken(token)admin.auth.refresh()admin.onUnauthorized(async () => { const { token } = await admin.auth.refresh(); admin.setToken(token); return true })credentials: 'include'details.required_permissionspree-auth-permissionsadmin.setStore(storeId)X-Spree-Store-Idorders.complete/cancel/approveproducts.clone/bulkStatusUpdate/bulkAddToCategories/...prices.bulkUpsertstockLevels.bulkUpsertrolespermissions.list()exportsimportswebhookEndpointsapiKeyspackages/admin-sdk/src/admin-client.tsacceptance_urladmin.invitations.acceptanceLink(id)admin.sellers.invitations.acceptanceLink(sellerId, id)admin.invitations.createrole_id?expand=admin.stockLevelssl_…stockItemscountry_codestate_codecountry_isoprices: [{ currency: 'USD', amount: '29.99' }]getadmin.me.get()permission_keysadmin.store.get()admin.exports.create({ type, search_params })exports.get(id)Authorization.download()// app/api/webhooks/spree/route.ts (Node runtime)
import { verifyWebhookSignature, type WebhookEvent } from '@spree/sdk/webhooks'
import type { Order } from '@spree/sdk'
export async function POST(request: Request) {
const rawBody = await request.text() // raw bytes, before JSON.parse
const ok = verifyWebhookSignature(
rawBody,
request.headers.get('x-spree-webhook-signature') ?? '',
request.headers.get('x-spree-webhook-timestamp') ?? '',
process.env.SPREE_WEBHOOK_SECRET!,
300, // replay tolerance (seconds)
)
if (!ok) return new Response('Unauthorized', { status: 401 })
const event = JSON.parse(rawBody) as WebhookEvent<Order>
if (event.name === 'order.placed') await syncToERP(event.data)
return Response.json({ ok: true })
}order.placedorder.completeddeprecated_alias_ofnode:crypto`${timestamp}.${rawBody}`spree-events-webhooksevent.idevent.idfetchclient.request/api/v3/store/api/v3/adminrequesttransformListParamsimport type { PaginatedResponse } from '@spree/sdk'
type Brand = { id: string; name: string; slug: string }
const { data } = await client.request<PaginatedResponse<Brand>>('GET', '/brands', {
params: { 'q[name_cont]': 'acme', page: 1, limit: 25 },
})
const created = await admin.request<Brand>('POST', '/brands', { body: { name: 'Acme' } })import type { RequestFn, PaginatedResponse } from '@spree/sdk'
export class BrandsClient {
constructor(private readonly request: RequestFn) {}
list(params: { page?: number; limit?: number; name_cont?: string } = {}) {
const { page, limit, ...predicates } = params
const q = Object.fromEntries(Object.entries(predicates).map(([k, v]) => [`q[${k}]`, v]))
return this.request<PaginatedResponse<Brand>>('GET', '/brands', { params: { page, limit, ...q } })
}
get(id: string) { return this.request<Brand>('GET', `/brands/${id}`) }
}@spree/admin-sdkRequestFnAdminClient['request']import { createClient, type Client, type ClientConfig } from '@spree/sdk'
export interface ExtendedClient extends Client { brands: BrandsClient }
export function createExtendedClient(config: ClientConfig): ExtendedClient {
const client = createClient(config) as ExtendedClient
client.brands = new BrandsClient(client.request)
return client
}Object.assign(base, { products: { ...base.products, republish(id: string) { ... } } }){ ...base }spree:api_resourcebundle exec rake typelizer:generatefetchserver.use(
http.get('https://test.spree.local/api/v3/store/products', () =>
HttpResponse.json({ data: [{ id: 'prod_test1', name: 'Test' }], meta: { page: 1, limit: 25, count: 1, pages: 1 } })),
)
const client = createClient({ baseUrl: 'https://test.spree.local', publishableKey: 'pk_test' })carts.getorders.get(cartId)nullonUnauthorizederr.details.required_scope@spree/admin-sdksecretKeyorder.completedorder.placedpackages/sdk/src/store-client.tspackages/admin-sdk/src/admin-client.tspackages/seller-sdk/src/seller-client.tsnode_modules/@spree/docs/dist/developer/sdk/admin/quickstart.mdadmin/authentication.mdnode_modules/@spree/docs/dist/api-reference/store.yamladmin.yamlseller.yamlspree-api-v3spree-storefrontspree-checkoutspree-events-webhooksspree-auth-permissionsspree-marketplace