---
title: "Filter, sort, paginate, and expand Admin API responses"
sidebarTitle: "Querying"
description: "Query Spree Admin API list endpoints with Ransack filters, sorting, pagination parameters, and the expand option to include related resources."
---

The Admin API uses [Ransack](https://activerecord-hackery.github.io/ransack/) for filtering and sorting on collection endpoints. All filter conditions go through the `q` parameter; sorting and pagination are top-level params.

## Filtering

Pass filter conditions via `q`:


```typescript SDK
const orders = await client.orders.list({
  status_eq: 'complete',
  total_gteq: 100,
  email_cont: '@example.com',
})
```

```bash cURL
curl -G 'https://store.example.com/api/v3/admin/orders' \
  -H 'X-Spree-Api-Key: sk_xxx' \
  --data-urlencode 'q[status_eq]=complete' \
  --data-urlencode 'q[total_gteq]=100' \
  --data-urlencode 'q[email_cont]=@example.com'
```


> **NOTE:** The SDK automatically wraps filter keys in `q[...]` and appends `[]` for array values — pass flat params.

### Common predicates

| Predicate | Description | SDK | cURL |
|---|---|---|---|
| `eq` | Equals | `status_eq: 'complete'` | `q[status_eq]=complete` |
| `not_eq` | Not equals | `status_not_eq: 'canceled'` | `q[status_not_eq]=canceled` |
| `cont` | Contains (case-insensitive) | `email_cont: '@acme'` | `q[email_cont]=@acme` |
| `start` | Starts with | `number_start: 'R10'` | `q[number_start]=R10` |
| `end` | Ends with | `slug_end: 'sale'` | `q[slug_end]=sale` |
| `lt` / `lteq` | Less than / less than or equal | `total_lteq: 50` | `q[total_lteq]=50` |
| `gt` / `gteq` | Greater than / greater than or equal | `total_gteq: 100` | `q[total_gteq]=100` |
| `in` | In a list | `status_in: ['complete', 'canceled']` | `q[status_in][]=complete&q[status_in][]=canceled` |
| `null` / `not_null` | Is null / not null | `completed_at_not_null: true` | `q[completed_at_not_null]=true` |
| `true` / `false` | Boolean | `accepts_email_marketing_true: 1` | `q[accepts_email_marketing_true]=1` |

### Prefixed IDs in filters

Resource ID filters accept Stripe-style prefixed IDs directly. The server decodes them before querying:


```typescript SDK
// All orders for a specific customer
const orders = await client.orders.list({
  user_id_eq: 'cus_UkLWZg9DAJ',
})
```

```bash cURL
curl -G 'https://store.example.com/api/v3/admin/orders' \
  -H 'X-Spree-Api-Key: sk_xxx' \
  --data-urlencode 'q[user_id_eq]=cus_UkLWZg9DAJ'
```


The same applies to `_id_in`, `_id_not_eq`, and other ID predicates.

### Combining filters

Multiple filters combine with AND:


```typescript SDK
// Completed orders over $100 from the last 7 days
const orders = await client.orders.list({
  status_eq: 'complete',
  total_gteq: 100,
  completed_at_gteq: new Date(Date.now() - 7 * 86_400_000).toISOString(),
})
```

```bash cURL
curl -G 'https://store.example.com/api/v3/admin/orders' \
  -H 'X-Spree-Api-Key: sk_xxx' \
  --data-urlencode 'q[status_eq]=complete' \
  --data-urlencode 'q[total_gteq]=100' \
  --data-urlencode 'q[completed_at_gteq]=2026-04-22T00:00:00Z'
```


### Filtering by association

Use underscore notation to filter on associated model attributes:


```typescript SDK
// Customers tagged as "wholesale"
const customers = await client.customers.list({
  tags_name_eq: 'wholesale',
})

// Products in a specific category
const products = await client.products.list({
  taxons_id_eq: 'ctg_xxx',
})
```

```bash cURL
curl -G 'https://store.example.com/api/v3/admin/customers' \
  -H 'X-Spree-Api-Key: sk_xxx' \
  --data-urlencode 'q[tags_name_eq]=wholesale'
```


### Custom search scopes

Some resources expose convenience search scopes:

| Resource | Scope | Example |
|---|---|---|
| Customers | `search` | Full-text over email + first/last name |
| Customers | `with_min_total_spent` | Filter by lifetime spend |
| Orders | `multi_search` | Number + email full-text |


```typescript SDK
const customers = await client.customers.list({
  search: 'jane',
})

const vipCustomers = await client.customers.list({
  with_min_total_spent: 1000,
})
```

```bash cURL
curl -G 'https://store.example.com/api/v3/admin/customers' \
  -H 'X-Spree-Api-Key: sk_xxx' \
  --data-urlencode 'q[search]=jane'
```


## Sorting

Use the top-level `sort` parameter on any list endpoint. Prefix with `-` for descending. Follows the [JSON:API sorting convention](https://jsonapi.org/format/#fetching-sorting).


```typescript SDK
// Most recent orders first
const orders = await client.orders.list({
  sort: '-completed_at',
})

// Multiple sort keys (priority left to right)
const customers = await client.customers.list({
  sort: '-total_spent,email',
})
```

```bash cURL
curl -G 'https://store.example.com/api/v3/admin/orders' \
  -H 'X-Spree-Api-Key: sk_xxx' \
  -d 'sort=-completed_at'
```


Sortable columns are limited to those whitelisted on the model (Ransack's `whitelisted_ransackable_attributes`). Sorting on a virtual column (e.g., a serializer-computed field like `display_total_spent`) is not supported.

## Pagination

All collection endpoints return paginated results. Control with `page` and `limit`:


```typescript SDK
const { data: orders, meta } = await client.orders.list({
  page: 2,
  limit: 50,
})

// meta.count, meta.pages, meta.previous, meta.next ...
```

```bash cURL
curl -G 'https://store.example.com/api/v3/admin/orders' \
  -H 'X-Spree-Api-Key: sk_xxx' \
  -d 'page=2' \
  -d 'limit=50'
```


| Parameter | Default | Max | Description |
|---|---|---|---|
| `page` | `1` | — | Page number (1-indexed) |
| `limit` | `25` | `100` | Records per page |

### Pagination metadata

Responses include a `meta` object:

```json
{
  "data": [...],
  "meta": {
    "page": 2,
    "limit": 50,
    "count": 327,
    "pages": 7,
    "from": 51,
    "to": 100,
    "in": 50,
    "previous": 1,
    "next": 3
  }
}
```

| Field | Description |
|---|---|
| `page` | Current page number |
| `limit` | Records per page |
| `count` | Total number of matching records |
| `pages` | Total number of pages |
| `from` / `to` / `in` | Position range / count of this page |
| `previous` / `next` | Previous/next page number, or `null` |

## Expanding associations

Most admin endpoints return slim payloads by default — associations are returned as IDs. Use the `expand` parameter to include related resources inline:


```typescript SDK
const order = await client.orders.get('or_xxx', {
  expand: ['items', 'fulfillments', 'payments', 'customer'],
})

// Nested expand
const customer = await client.customers.get('cus_xxx', {
  expand: ['addresses', 'store_credits'],
})
```

```bash cURL
curl -G 'https://store.example.com/api/v3/admin/orders/or_xxx' \
  -H 'X-Spree-Api-Key: sk_xxx' \
  -d 'expand=items,fulfillments,payments,customer'
```


### Nested expand

Use dot notation up to 4 levels deep:

```typescript
const order = await client.orders.get('or_xxx', {
  expand: ['items.variant.product', 'fulfillments.tracking'],
})
```

A nested expand implicitly includes its parent — `expand: ['items.variant']` returns both `items` and their `variant` data.

### What can I expand?

Each endpoint documents its supported expand keys in the OpenAPI reference. Common admin expansions:

| Resource | Common expands |
|---|---|
| Orders | `items`, `fulfillments`, `payments`, `customer`, `discounts`, `adjustments`, `billing_address`, `shipping_address` |
| Customers | `addresses`, `store_credits`, `orders` |
| Products | `variants`, `media`, `option_types`, `categories` |

## Field selection

Use the `fields` parameter to request only specific fields on a resource. Reduces payload size for bandwidth-sensitive integrations:


```typescript SDK
const orders = await client.orders.list({
  fields: ['number', 'total', 'status', 'completed_at'],
})
```

```bash cURL
curl -G 'https://store.example.com/api/v3/admin/orders' \
  -H 'X-Spree-Api-Key: sk_xxx' \
  -d 'fields=number,total,status,completed_at'
```


Rules:

- `id` is always included
- Expanded associations return their full payload regardless of `fields`
- Field selection applies to the top-level resource only

> **NOTE:** TypeScript types in `@spree/admin-sdk` remain fully typed regardless of `fields`. At runtime, only the requested fields are present — the rest are `undefined`.
