The Store API uses Ransack for filtering and sorting collection endpoints. All query parameters are passed via the q parameter.
Filtering
Pass filter conditions using the q parameter with Ransack predicates:
const products = await client.products.list({
name_cont: 'shirt',
price_gte: 20,
price_lte: 100,
})
Common Predicates
| Predicate | Description | SDK | cURL |
|---|
eq | Equals | status_eq: 'active' | q[status_eq]=active |
not_eq | Not equals | status_not_eq: 'draft' | q[status_not_eq]=draft |
cont | Contains (case-insensitive) | name_cont: 'shirt' | q[name_cont]=shirt |
start | Starts with | name_start: 'Spree' | q[name_start]=Spree |
end | Ends with | slug_end: 'tote' | q[slug_end]=tote |
lt | Less than | price_lt: 50 | q[price_lt]=50 |
lteq | Less than or equal | price_lteq: 50 | q[price_lteq]=50 |
gt | Greater than | price_gt: 10 | q[price_gt]=10 |
gteq | Greater than or equal | price_gteq: 10 | q[price_gteq]=10 |
in | In a list | status_in: ['active', 'draft'] | q[status_in][]=active&q[status_in][]=draft |
null | Is null | deleted_at_null: true | q[deleted_at_null]=true |
not_null | Is not null | completed_at_not_null: true | q[completed_at_not_null]=true |
present | Is present (not empty) | description_present: true | q[description_present]=true |
blank | Is blank (null or empty) | description_blank: true | q[description_blank]=true |
true | Is true (boolean) | purchasable_true: 1 | q[purchasable_true]=1 |
false | Is false (boolean) | purchasable_false: 1 | q[purchasable_false]=1 |
The SDK automatically wraps filter keys in q[...] and appends [] for array values — just pass flat params.
Combining Filters
Multiple filters are combined with AND logic:
// Products that contain "shirt" AND cost between $20-$100
const products = await client.products.list({
name_cont: 'shirt',
price_gteq: 20,
price_lteq: 100,
})
Filtering by Association
You can filter by associated model attributes using underscore notation:
// Products in a specific category
const products = await client.products.list({
categories_id_eq: 'ctg_abc123',
})
// Categories at a specific depth
const categories = await client.categories.list({
depth_eq: 1,
})
Product Scopes
Products support additional filter scopes beyond standard Ransack predicates:
const products = await client.products.list({
price_gte: 20, // Minimum price
price_lte: 100, // Maximum price
with_option_value_ids: ['optval_abc', 'optval_def'], // Filter by option values
in_stock: true, // Only in-stock products
})
| Scope | Description | SDK | cURL |
|---|
price_gte | Minimum price | price_gte: 20 | q[price_gte]=20 |
price_lte | Maximum price | price_lte: 100 | q[price_lte]=100 |
with_option_value_ids | Filter by option value IDs | with_option_value_ids: ['optval_abc'] | q[with_option_value_ids][]=optval_abc |
in_stock | Only in-stock products | in_stock: true | q[in_stock]=true |
out_of_stock | Only out-of-stock products | out_of_stock: true | q[out_of_stock]=true |
search | Full-text search | search: 'shirt' | q[search]=shirt |
Sorting
Use the sort parameter on any list endpoint. Prefix a field with - for descending order (ascending is the default). This follows the JSON:API sorting convention.
// Sort by price (low to high)
const products = await client.products.list({
sort: 'price',
})
// Sort by price (high to low)
const products = await client.products.list({
sort: '-price',
})
// Sort by best selling
const products = await client.products.list({
sort: 'best_selling',
})
Product Sort Options
| Value | Description |
|---|
manual | Manual sort order (default, uses taxon position) |
best_selling | Best selling products first |
price | Price low to high |
-price | Price high to low |
name | Name A-Z |
-name | Name Z-A |
-available_on | Newest first |
available_on | Oldest first |
Sorting Other Resources
The sort parameter works on all list endpoints. Use any sortable column name:
// Categories sorted by name
const categories = await client.categories.list({
sort: 'name',
})
// Customer orders sorted by most recent
const orders = await client.customer.orders.list({
sort: '-completed_at',
})
Expanding Associations
Use the expand parameter to include associated resources in the response. Pass a comma-separated list of association names:
const product = await client.products.get('spree-tote', {
expand: ['variants', 'images'],
})
Nested Expand
Use dot notation to expand nested associations (up to 4 levels deep):
// Expand variants and their images in one request
const product = await client.products.get('spree-tote', {
expand: ['variants.images'],
})
// Multiple nested expands
const product = await client.products.get('spree-tote', {
expand: ['variants.images', 'variants.metafields', 'option_types'],
})
// Category with nested children
const category = await client.categories.get('clothing/shirts', {
expand: ['children.children'], // Two levels of subcategories
})
Rules
- Maximum depth is 4 levels (e.g.,
a.b.c.d)
- A nested expand automatically includes its parent —
expand=variants.images expands both variants and their images
- Nested expand only applies to conditional associations (those controlled by
expand)
Field Selection
Use the fields parameter to request only specific fields in the response. This reduces payload size for bandwidth-sensitive clients like mobile apps or server-side rendering.
// Only return name, slug, and price
const products = await client.products.list({
fields: ['name', 'slug', 'price', 'display_price'],
})
// Works on single resources too
const product = await client.products.get('spree-tote', {
fields: ['name', 'slug', 'price'],
})
{
"data": [
{
"id": "prod_86Rf07xd4z",
"name": "Spree Tote",
"slug": "spree-tote",
"price": "15.99",
"display_price": "$15.99"
}
],
"meta": { ... }
}
Rules
- The
id field is always included, even if not listed in fields
- Omitting
fields returns all fields (default behavior)
- Field selection applies to the top-level resource only — expanded associations always return their full set of fields
- Expanded associations are always included in the response regardless of
fields — ?fields=name&expand=variants returns id, name, and variants
The SDK TypeScript types remain fully typed regardless of field selection. When using fields, be aware that only the requested fields will be present at runtime.
All collection endpoints return paginated results. Control pagination with page and limit parameters:
const { data: products, meta } = await client.products.list({
page: 2,
limit: 10,
})
// meta contains pagination info
console.log(meta)
// {
// page: 2,
// limit: 10,
// count: 85,
// pages: 9,
// from: 11,
// to: 20,
// in: 10,
// previous: 1,
// next: 3
// }
| Parameter | Default | Max | Description |
|---|
page | 1 | - | Page number |
limit | 25 | 100 | Number of records per page |
Collection responses include a meta object with pagination info:
{
"data": [...],
"meta": {
"page": 1,
"limit": 25,
"count": 85,
"pages": 4,
"from": 1,
"to": 25,
"in": 25,
"previous": null,
"next": 2
}
}
| Field | Description |
|---|
page | Current page number |
limit | Records per page |
count | Total number of records |
pages | Total number of pages |
from | Starting record number on this page |
to | Ending record number on this page |
in | Number of records returned on this page |
previous | Previous page number (null if first page) |
next | Next page number (null if last page) |