packages/sdk/README.md
Official TypeScript SDK for Spree Commerce API v3.
npm install @spree/sdk
# or
yarn add @spree/sdk
# or
pnpm add @spree/sdk
import { createClient } from '@spree/sdk';
// Initialize the client
const client = createClient({
baseUrl: 'https://api.mystore.com',
publishableKey: 'spree_pk_xxx',
});
// Browse products (Store API)
const products = await client.products.list({
limit: 10,
expand: ['variants', 'media'],
});
// Get a single product
const product = await client.products.get('spree-tote');
// Authentication
const { token, user } = await client.auth.login({
email: '[email protected]',
password: 'password123',
});
// Create a cart and add items
const cart = await client.carts.create();
await client.carts.items.create(cart.id, {
variant_id: 'var_abc123',
quantity: 2,
}, { spreeToken: cart.token });
// Update cart and complete
await client.carts.update(cart.id, {
email: '[email protected]',
}, { spreeToken: cart.token });
await client.carts.complete(cart.id, { spreeToken: cart.token });
All Store API resources are available directly on the client:
client.products.list() // Products
client.carts.create() // Create a cart
client.carts.get(cartId) // Get a cart by ID
client.carts.items.create(cartId, params) // Cart line items
client.carts.complete(cartId, opt) // Complete cart
client.carts.list(opt) // List active carts
client.customers.create(params) // Registration
client.customer.get(opt) // Account
client.customer.orders.list() // Order history
The SDK supports multiple authentication modes:
const client = createClient({
baseUrl: 'https://api.mystore.com',
publishableKey: 'spree_pk_xxx',
});
// Public endpoints work without user authentication
const products = await client.products.list();
// Login to get tokens
const { token, user } = await client.auth.login({
email: '[email protected]',
password: 'password123',
});
// Use token for authenticated requests
const orders = await client.customer.orders.list({}, { token });
// Refresh token when needed
const newTokens = await client.auth.refresh({ token });
const { token, user } = await client.customers.create({
email: '[email protected]',
password: 'password123',
password_confirmation: 'password123',
first_name: 'John',
last_name: 'Doe',
});
For guest checkout, use the token returned when creating a cart:
// Create a cart (guest)
const cart = await client.carts.create();
// Use spreeToken for all cart operations
const options = { spreeToken: cart.token };
// Add items
await client.carts.items.create(cart.id, {
variant_id: 'var_abc123',
quantity: 1,
}, options);
// Update cart with email and addresses
await client.carts.update(cart.id, {
email: '[email protected]',
}, options);
// Complete the order
await client.carts.complete(cart.id, options);
// Get current store information
const store = await client.store.get();
// List products with filtering
const products = await client.products.list({
page: 1,
limit: 25,
name_cont: 'shirt',
sort: 'price asc',
expand: ['variants', 'media', 'categories'],
});
// Get single product by ID or slug
const product = await client.products.get('spree-tote', {
expand: ['variants', 'media'],
});
// Get product with prior price (EU Omnibus Directive compliance)
const product = await client.products.get('spree-tote', {
expand: ['prior_price'],
});
console.log(product.prior_price); // { amount: "9.99", currency: "USD", display_amount: "$9.99", ... }
// Get available filters (price range, availability, options, categories)
const filters = await client.products.filters({
category_id: 'ctg_abc123', // Optional: scope filters to a category
});
// List categories with filtering
const categories = await client.categories.list({
depth_eq: 1, // Top-level categories only
});
// Get single category by ID or permalink
const category = await client.categories.get('clothing/shirts', {
expand: ['ancestors', 'children'], // For breadcrumbs and subcategories
});
// List products in a category
const categoryProducts = await client.categories.products.list('clothing/shirts', {
page: 1,
limit: 12,
expand: ['media', 'default_variant'],
});
All cart operations use explicit cart IDs (cart_xxx). The cart is authorized
via spreeToken header (guest) or JWT token (authenticated user).
// Create a new cart
const cart = await client.carts.create();
// Get a cart by ID
const cart = await client.carts.get('cart_xxx', { spreeToken: cart.token });
// Delete / abandon a cart
await client.carts.delete('cart_xxx', { spreeToken: cart.token });
// Associate guest cart with authenticated user
await client.carts.associate('cart_xxx', {
token: jwtToken, // User's JWT token
});
// List all active carts for authenticated user
const { data: carts } = await client.carts.list({ token: jwtToken });
// Update cart (email, addresses, customer note)
await client.carts.update('cart_xxx', {
email: '[email protected]',
shipping_address: {
first_name: 'John',
last_name: 'Doe',
address1: '123 Main St',
city: 'New York',
postal_code: '10001',
phone: '+1 555 123 4567',
country_iso: 'US',
state_abbr: 'NY',
},
}, options);
// Complete the order
await client.carts.complete('cart_xxx', options);
const options = { spreeToken: cart.token };
// Add item
await client.carts.items.create('cart_xxx', {
variant_id: 'var_123',
quantity: 2,
}, options);
// Update item quantity
await client.carts.items.update('cart_xxx', lineItemId, {
quantity: 3,
}, options);
// Remove item
await client.carts.items.delete('cart_xxx', lineItemId, options);
const options = { spreeToken: cart.token };
// Apply a discount code
await client.carts.discountCodes.apply('cart_xxx', 'SAVE20', options);
// Remove a discount code
await client.carts.discountCodes.remove('cart_xxx', 'SAVE20', options);
const options = { spreeToken: cart.token };
// Apply a gift card
const cart = await client.carts.giftCards.apply('cart_xxx', 'GC-ABCD-1234', options);
// Remove a gift card (ID from cart.gift_card.id)
await client.carts.giftCards.remove('cart_xxx', 'gc_abc123', options);
// Check cart warnings (e.g. items removed due to stock changes)
if (cart.warnings.length > 0) {
cart.warnings.forEach(w => console.log(w.code, w.message));
}
const options = { spreeToken: cart.token };
// Fulfillments are included in the cart response.
// Select a delivery rate
await client.carts.fulfillments.update('cart_xxx', fulfillmentId, {
selected_delivery_rate_id: 'rate_xxx',
}, options);
const options = { spreeToken: cart.token };
// Apply store credit (applies maximum available by default)
await client.carts.storeCredits.apply('cart_xxx', undefined, options);
// Apply specific amount of store credit
await client.carts.storeCredits.apply('cart_xxx', 25.00, options);
// Remove store credit
await client.carts.storeCredits.remove('cart_xxx', options);
const options = { spreeToken: cart.token };
// Payment methods and payments are included in the cart response.
// Each payment method includes `session_required` flag:
// - true -> use paymentSessions (Stripe, Adyen, PayPal, etc.)
// - false -> use payments.create (Check, Cash on Delivery, Bank Transfer, etc.)
// Create a payment for a non-session payment method
// (e.g. Check, Cash on Delivery, Bank Transfer, Purchase Order)
const payment = await client.carts.payments.create('cart_xxx', {
payment_method_id: 'pm_xxx',
amount: '99.99', // Optional, defaults to order total minus store credits
metadata: { // Optional, write-only metadata
purchase_order_number: 'PO-12345',
},
}, options);
Payment sessions provide a unified, provider-agnostic interface for payment processing. They work with any payment gateway (Stripe, Adyen, PayPal, etc.) through a single API.
const options = { spreeToken: cart.token };
// Create a payment session (initializes a session with the payment gateway)
const session = await client.carts.paymentSessions.create('cart_xxx', {
payment_method_id: 'pm_xxx',
amount: '99.99', // Optional, defaults to order total
external_data: { // Optional, provider-specific data
return_url: 'https://mystore.com/checkout/complete',
},
}, options);
// The session contains provider-specific data (e.g., Stripe client_secret)
console.log(session.external_data.client_secret);
// Get a payment session
const existing = await client.carts.paymentSessions.get(
'cart_xxx', session.id, options
);
// Update a payment session (e.g., after order total changes)
await client.carts.paymentSessions.update('cart_xxx', session.id, {
amount: '149.99',
}, options);
// Complete the payment session (after customer confirms payment on the frontend)
const completed = await client.carts.paymentSessions.complete(
'cart_xxx',
session.id,
{ session_result: 'success' },
options
);
console.log(completed.status); // 'completed'
Completed orders can be looked up by ID or number:
// Get a completed order by ID or number
const order = await client.orders.get('R123456789', {
expand: ['items', 'fulfillments'],
}, { spreeToken: orderToken });
For order history, use the customer orders resource:
const options = { token: jwtToken };
// List order history for authenticated customer
const orders = await client.customer.orders.list({}, options);
// Get a specific order from history
const order = await client.customer.orders.get('or_xxx', {
expand: ['items', 'fulfillments'],
}, options);
// List all markets
const { data: markets } = await client.markets.list();
// [{ id: "mkt_xxx", name: "North America", currency: "USD", default_locale: "en", ... }]
// Get a single market
const market = await client.markets.get('mkt_xxx');
// Resolve which market applies for a country
const market = await client.markets.resolve('DE');
// => { id: "mkt_xxx", name: "Europe", currency: "EUR", default_locale: "de", ... }
// List countries in a market
const { data: countries } = await client.markets.countries.list('mkt_xxx');
// Get a country in a market (with states for address forms)
const country = await client.markets.countries.get('mkt_xxx', 'DE', {
expand: ['states'],
});
// List countries available for checkout
const { data: countries } = await client.countries.list();
// Get country by ISO code (with states)
const usa = await client.countries.get('US', { expand: ['states'] });
console.log(usa.states); // Array of states
const options = { token: jwtToken };
// Get profile
const profile = await client.customer.get(options);
// Update profile
await client.customer.update({
first_name: 'John',
last_name: 'Doe',
}, options);
const options = { token: jwtToken };
// List addresses
const { data: addresses } = await client.customer.addresses.list({}, options);
// Get address by ID
const address = await client.customer.addresses.get('addr_xxx', options);
// Create address
await client.customer.addresses.create({
first_name: 'John',
last_name: 'Doe',
address1: '123 Main St',
city: 'New York',
postal_code: '10001',
country_iso: 'US',
state_abbr: 'NY',
}, options);
// Update address
await client.customer.addresses.update('addr_xxx', { city: 'Brooklyn' }, options);
// Delete address
await client.customer.addresses.delete('addr_xxx', options);
// Set as default billing or shipping address
await client.customer.addresses.update('addr_xxx', { is_default_billing: true }, options);
await client.customer.addresses.update('addr_xxx', { is_default_shipping: true }, options);
// Request a password reset email
// Always returns { message: string } (202 status) — prevents email enumeration
await client.passwordResets.create({
email: '[email protected]',
redirect_url: 'https://myshop.com/reset-password', // optional, validated against store's allowed origins
});
// Reset password with token from email
// Returns AuthTokens (JWT + user) — auto-login on success
// Token expires in 15 minutes
const { token, user } = await client.passwordResets.update('reset_token_xxx', {
password: 'newPassword123',
password_confirmation: 'newPassword123',
});
const options = { token: jwtToken };
// List saved credit cards
const { data: cards } = await client.customer.creditCards.list({}, options);
// Get credit card by ID
const card = await client.customer.creditCards.get('cc_xxx', options);
// Delete credit card
await client.customer.creditCards.delete('cc_xxx', options);
const options = { token: jwtToken };
// List wishlists
const { data: wishlists } = await client.wishlists.list({}, options);
// Get wishlist by ID
const wishlist = await client.wishlists.get('wl_xxx', {
expand: ['wishlist_items'],
}, options);
// Create wishlist
const newWishlist = await client.wishlists.create({
name: 'Birthday Ideas',
is_private: true,
}, options);
// Update wishlist
await client.wishlists.update('wl_xxx', {
name: 'Updated Name',
}, options);
// Delete wishlist
await client.wishlists.delete('wl_xxx', options);
const options = { token: jwtToken };
// Add item to wishlist
await client.wishlists.items.create('wl_xxx', {
variant_id: 'var_123',
quantity: 1,
}, options);
// Update item quantity
await client.wishlists.items.update('wl_xxx', 'wi_xxx', {
quantity: 2,
}, options);
// Remove item from wishlist
await client.wishlists.items.delete('wl_xxx', 'wi_xxx', options);
The SDK uses a resource builder pattern for nested resources:
| Parent Resource | Nested Resource | Available Methods |
|---|---|---|
carts | items | create, update, delete |
carts | discountCodes | apply, remove |
carts | giftCards | apply, remove |
carts | fulfillments | update |
carts | payments | create |
carts | paymentSessions | create, get, update, complete |
carts | storeCredits | apply, remove |
policies | — | list, get |
passwordResets | — | create, update |
customer | addresses | list, get, create, update, delete |
customer | creditCards | list, get, delete |
customer | giftCards | list, get |
customer | storeCredits | list, get |
customer | orders | list, get |
markets | countries | list, get |
categories | products | list |
wishlists | items | create, update, delete |
Example:
// Cart resources take cartId as first argument
await client.carts.items.create(cartId, params, options);
await client.carts.discountCodes.apply(cartId, code, options);
await client.carts.fulfillments.update(cartId, fulfillmentId, params, options);
await client.carts.payments.create(cartId, params, options);
await client.carts.paymentSessions.create(cartId, params, options);
await client.carts.storeCredits.apply(cartId, amount, options);
// Other nested resources follow the same pattern
await client.customer.addresses.list({}, options);
await client.customer.orders.list({}, options);
await client.markets.countries.list(marketId);
await client.categories.products.list(categoryId, params, options);
await client.wishlists.items.create(wishlistId, params, options);
Set locale, currency, and country when creating the client:
const client = createClient({
baseUrl: 'https://api.mystore.com',
publishableKey: 'spree_pk_xxx',
locale: 'fr',
currency: 'EUR',
country: 'FR',
});
// All requests use fr/EUR/FR automatically
const products = await client.products.list();
Update defaults at any time:
client.setLocale('de');
client.setCurrency('EUR');
client.setCountry('DE');
Pass locale and currency headers with any request to override defaults:
const products = await client.products.list({}, {
locale: 'fr',
currency: 'EUR',
country: 'FR',
});
import { SpreeError } from '@spree/sdk';
try {
await client.products.get('non-existent');
} catch (error) {
if (error instanceof SpreeError) {
console.log(error.code); // 'record_not_found'
console.log(error.message); // 'Product not found'
console.log(error.status); // 404
console.log(error.details); // Validation errors (if any)
}
}
The SDK includes full TypeScript support with generated types from the API serializers:
import type {
Product,
Cart,
Order,
Variant,
Category,
LineItem,
Address,
Customer,
PaginatedResponse,
} from '@spree/sdk';
// All responses are fully typed
const products: PaginatedResponse<Product> = await client.products.list();
const category: Category = await client.categories.get('clothing');
const cart: Cart = await client.carts.get('cart_xxx');
All types are exported as unprefixed names (e.g., Product, Order). Legacy Store* prefixed aliases (e.g., StoreProduct) are still available for backward compatibility.
Product - Product dataVariant - Variant dataCart - Cart data (uses cart_ prefixed IDs)Order - Completed order data (uses or_ prefixed IDs)LineItem - Line item in cartCategory - CategoryCountry - Country with statesState - State/provinceAddress - Customer addressCustomer - Customer profileMarket - Market configuration (currency, locales, countries)Payment - Payment recordPaymentMethod - Payment methodPaymentSession - Provider-agnostic payment sessionFulfillment - Fulfillment recordDeliveryRate - Delivery rate optionDeliveryMethod - Delivery methodCreditCard - Saved credit cardGiftCard - Gift cardDiscount - Discount applied to a cart or orderMedia - Product media (images, videos)Price - Price dataPriceHistory - Prior price data (for EU Omnibus Directive compliance)OptionType - Option type (e.g., Size, Color)OptionValue - Option value (e.g., Small, Red)DigitalLink - Digital download linkCustomField - Custom field dataWishlist - WishlistWishlistItem - Wishlist itemClient - Main client interfaceStoreClient - Store API client classClientConfig - Client configurationRequestOptions - Per-request optionsRetryConfig - Retry behavior configurationPaginatedResponse<T> - Paginated API responseListResponse<T> - List API responseAuthTokens - JWT tokens from loginAddressParams - Address input parametersUpdateCartParams - Cart update parameters (email, addresses, etc.)CreatePaymentParams - Direct payment creation parameters (for non-session payment methods)CreatePaymentSessionParams - Payment session creation parametersUpdatePaymentSessionParams - Payment session update parametersCompletePaymentSessionParams - Payment session completion parametersProductFiltersResponse - Product filters responseCheckoutRequirement - Checkout requirement ({ step, field, message })You can provide a custom fetch implementation:
import { createClient } from '@spree/sdk';
const client = createClient({
baseUrl: 'https://api.mystore.com',
publishableKey: 'spree_pk_xxx',
fetch: customFetchImplementation,
});
The SDK includes webhook signature verification and types via the @spree/sdk/webhooks subpath:
import { verifyWebhookSignature, type WebhookEvent } from '@spree/sdk/webhooks';
import type { Order } from '@spree/sdk';
// Verify a webhook signature (works with any framework)
const isValid = verifyWebhookSignature(
rawBody, // raw request body string
request.headers.get('x-spree-webhook-signature')!, // HMAC signature
request.headers.get('x-spree-webhook-timestamp')!, // unix timestamp
process.env.SPREE_WEBHOOK_SECRET! // endpoint secret key
);
// Type your webhook handlers using SDK types
type OrderEvent = WebhookEvent<Order>;
function handleOrderCompleted(event: OrderEvent) {
const order = event.data; // fully typed Order
console.log(order.number, order.email, order.display_total);
}
Webhook payloads use the same V3 serializers as the REST API, so all SDK types (Order, Cart, Payment, Fulfillment, etc.) work directly as the data field type.
For Next.js projects, the Spree Storefront includes a ready-made webhook route handler with signature verification and event routing.
cd sdk
npm install
| Command | Description |
|---|---|
npm test | Run tests once |
npm run test:watch | Run tests in watch mode |
npm run test:coverage | Run tests with coverage report |
npm run typecheck | Type-check with tsc --noEmit |
npm run build | Build CJS + ESM bundles with tsup |
npm run dev | Build in watch mode |
npm run console | Interactive REPL for testing the SDK |
Tests use Vitest with MSW (Mock Service Worker) for API mocking at the network level.
# Run all tests
npm test
# Run in watch mode during development
npm run test:watch
# Run with coverage
npm run test:coverage
Test files live in tests/ and follow the structure:
tests/mocks/handlers.ts - MSW request handlers with fixture datatests/mocks/server.ts - MSW server instancetests/setup.ts - Server lifecycle (listen/reset/close)tests/helpers.ts - createTestClient() and constantstests/*.test.ts - Test suites per resource (auth, products, orders, etc.)To add tests for a new endpoint, add an MSW handler in handlers.ts and create a corresponding test file.
This package uses Changesets for version management and publishing.
After making changes:
npx changeset
This prompts you to select a semver bump type (patch/minor/major) and write a summary. A changeset file is created in .changeset/.
How releases work:
main, a GitHub Action creates a "Version Packages" PR that bumps the version and updates the CHANGELOGManual release (if needed):
npm run version # Apply changesets and bump version
npm run release # Build and publish to npm
MIT