Skip to main content

Filtering & Sorting

List endpoints support three tiers of filtering, from simple equality to nested AND/OR trees, plus sorting and search. All tiers compose in one request.

Tier 1: Simple filters​

Bare column name, exact match:

# Unpaid purchase invoices
curl "https://acme.sku.io/api/purchase-invoices?filter[status]=unpaid" \
-H "Authorization: Bearer $SKU_TOKEN" -H "Accept: application/json"

# Standard-type products
curl "https://acme.sku.io/api/v2/products?filter[type]=standard" \
-H "Authorization: Bearer $SKU_TOKEN" -H "Accept: application/json"

Multiple filters are ANDed together. filter[status]=unpaid is shorthand for filter[status.is]=unpaid.

Tier 2: Operator filters​

Add an operator after the column with a dot: filter[column.operator]=value.

# Unpaid invoices from suppliers matching "Default", created after June 1
curl "https://acme.sku.io/api/purchase-invoices?filter[status.is]=unpaid&filter[supplier_name.contains]=Default&filter[created_at.after]=2026-06-01" \
-H "Authorization: Bearer $SKU_TOKEN" -H "Accept: application/json"

Text operators​

OperatorExampleMatches
is / is_notfilter[status.is]=paidequality / inequality
contains / does_not_containfilter[name.contains]=widgetsubstring
starts_with / does_not_start_withfilter[sku.starts_with]=WDGprefix
ends_with / does_not_end_withfilter[sku.ends_with]=-USsuffix
is_one_offilter[status.is_one_of]=paid,unpaidIN list (comma-separated)
is_empty / is_not_emptyfilter[barcode.is_empty]=1null or empty string

Numeric operators​

OperatorExample
is, is_not, is_one_offilter[id.is_one_of]=1,2,3
greater_than, less_thanfilter[unit_cost.greater_than]=20
greater_than_or_equal, less_than_or_equalfilter[unit_cost.less_than_or_equal]=10
betweenfilter[unit_cost.between]=10,20
is_empty, is_not_emptyfilter[quantity.is_empty]=1

Date operators​

OperatorExample
is, is_notfilter[due_date.is]=2026-12-24
before, afterfilter[created_at.after]=2026-06-01
on_or_before, on_or_afterfilter[due_date.on_or_before]=2026-12-31
betweenfilter[due_date.between]=2026-01-01,2026-12-31
today, yesterday, tomorrowfilter[due_date.today]=1
past_week, past_month, past_yearfilter[created_at.past_month]=1
next_week, next_month, next_yearfilter[due_date.next_week]=1
days_ago, days_from_nowfilter[created_at.days_ago]=7
past_days, next_daysfilter[created_at.past_days]=14
is_empty, is_not_emptyfilter[due_date.is_empty]=1

Datetime columns (created_at, updated_at, ...) are compared in your tenant's timezone and converted to UTC internally; date-only columns (due_date, ...) compare directly. See Dates & Timezones.

Discovering an endpoint's filters​

Filter columns are allowlisted per endpoint. Sending an unknown filter returns 400 with the complete allowed list — a deliberate discovery mechanism:

curl "https://acme.sku.io/api/purchase-invoices?filter[_discover]=1" \
-H "Authorization: Bearer $SKU_TOKEN" -H "Accept: application/json"
{
"message": "Requested filter(s) `_discover` are not allowed. Allowed filter(s) are `status, status.is, status.is_not, status.contains, supplier_name.contains, created_at.after, created_at.between, ...`"
}

Everything before the dot is a column; everything after is an operator; a bare name (no dot) means Tier 1 equality is supported.

Sorting​

Use sort with a field name; prefix - for descending:

curl "https://acme.sku.io/api/v2/products?sort=-created_at" \
-H "Authorization: Bearer $SKU_TOKEN" -H "Accept: application/json"

curl "https://acme.sku.io/api/v2/products?sort=sku" \
-H "Authorization: Bearer $SKU_TOKEN" -H "Accept: application/json"

Sortable fields are allowlisted per endpoint and listed in each endpoint's reference page (for example, GET /api/v2/products allows id, sku, name, type, unit_cost, average_cost, created_at, updated_at, and more). An unlisted sort field returns 400. Endpoints have a sensible default sort (usually newest first).

Many list endpoints accept filter[search] for cross-field text search:

# Searches ID (exact), SKU, name, barcode, MPN, and brand name on products
curl "https://acme.sku.io/api/v2/products?filter[search]=widget" \
-H "Authorization: Bearer $SKU_TOKEN" -H "Accept: application/json"

Tier 3: Grouped filter trees (advanced)​

For nested AND/OR logic that flat filters can't express, send a filter_groups tree. Selected endpoints accept it as a JSON POST body; all supporting endpoints accept it as a base64-encoded GET query parameter.

# Products whose SKU starts with "LB-" AND whose type is standard OR bundle
curl -X POST "https://acme.sku.io/api/v2/products" \
-H "Authorization: Bearer $SKU_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"filter_groups": {
"conjunction": "and",
"children": [
{"type": "condition", "condition": {"column": "sku", "operator": "starts_with", "value": "LB-"}},
{
"type": "group",
"group": {
"conjunction": "or",
"children": [
{"type": "condition", "condition": {"column": "type", "operator": "is", "value": "standard"}},
{"type": "condition", "condition": {"column": "type", "operator": "is", "value": "bundle"}}
]
}
}
]
},
"per_page": 25,
"sort": "-created_at"
}'

Rules:

  • Same columns and operators as Tiers 1–2; unknown columns in a tree are silently ignored.
  • Maximum nesting: root plus one level of sub-groups.
  • page, per_page, sort, and filter[search] compose with the tree (in the body or as query params).
  • Flat filters in the same request are ANDed with the tree result.
  • For GET, base64-encode the same JSON: ?filter_groups=eyJjb25qdW5jdGlvbiI6...

Which endpoints accept a POST-body tree?​

The base64 GET form (?filter_groups=...) works on any filterable list endpoint. The JSON POST-body form is a convenience path supported by a specific set of list endpoints:

List endpointPOST-body filter_groups
POST /api/v2/products✅
POST /api/v2/warehouse-transfers✅
POST /api/v2/inventory-adjustments✅
POST /api/v2/inventory-movements✅
POST /api/v2/fifo-layers✅
POST /api/v2/backorder-queues✅
POST /api/assemblies✅
POST /api/stock-takes/list✅
any other filterable list endpointuse the base64 GET form

If you need trees on an endpoint not in this table, encode the same JSON and pass it as the ?filter_groups= query parameter on the endpoint's GET.

Flat Tier 1/2 filters cover the vast majority of integration use cases — reach for trees only when you genuinely need OR logic.

Includes​

The API does not support a generic include parameter for loading relationships on demand. Each endpoint returns a fixed shape (relations the endpoint eager-loads are documented per endpoint in the reference).