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).