Search Products
The list endpoint at GET /api/shop/products doubles as the search & filter endpoint. Every storefront facet — keyword search, category, price range, attribute filters, sort, "new" / "featured" — is just a query parameter. This page is the complete reference for those parameters.
The GraphQL products field expresses the same filters differently — a JSON filter: string, sortKey / reverse, and cursor pagination instead of query parameters. See List Products on the GraphQL side.
Endpoint
GET /api/shop/productsSame URL as Products; the only difference is which query parameters you send.
Reserved parameters
These names are interpreted by the search/sort/pagination layer — never as attribute filters:
| Parameter | Type | Default | Description |
|---|---|---|---|
query | string | — | Search term. Matches against product SKU and product name. |
sort | string | — | Sort token. Either compound (name-asc, price-desc) or a bare key paired with order (sort=name&order=desc). See Sort tokens below. |
order | string | asc | Direction when sort is a bare key. Ignored if sort already has a -asc / -desc suffix. |
page | integer | 1 | 1-based page number. |
per_page | integer | 30 | Items per page. Max 50. |
locale | string | — | Locale override (alternative to the X-Locale header). |
channel | string | — | Channel override (alternative to the X-Channel header). |
filter | string | — | JSON filter object — GraphQL parity. Example: {"color":{"match":"3","match_type":"PARTIAL"}}. Most clients prefer the simpler ?<attribute>=<id> shorthand. |
type | string | — | Product type. One of simple, configurable, bundle, grouped, virtual, downloadable, booking. |
category_id | integer | — | Filter by category ID. categoryId (camelCase) is also accepted. |
price | string | — | Compound price range in the exact form {min},{max} — the comma separates minimum from maximum, it is not a thousands separator. ?price=10,200 means min 10, max 200. Use . for decimals and no grouping (e.g. ?price=1000,5000.50), never ?price=1,000. |
price_from | number | — | Minimum price (inclusive). Same as the {min} in price. |
price_to | number | — | Maximum price (inclusive). Same as the {max} in price. |
new | boolean | — | 1 to restrict to products with the "new" flag set. |
featured | boolean | — | 1 to restrict to products with the "featured" flag set. |
Attribute filters (anything else)
Any query parameter not listed above is treated as a filterable attribute code. Nothing needs changing when a merchant adds a new filterable attribute — the parameter works as soon as the attribute exists.
| Pattern | Example | Effect |
|---|---|---|
?<code>=<option_id> | ?color=3 | Single option ID match |
?<code>=id1,id2,… | ?size=4,5,6 | Multi-option (OR) match |
?brand=N, ?material=N | ?material=12 | Same shape — works for any filterable attribute |
?filter[color]=3&filter[size]=5 | (Swagger UI form) | API Platform's bracket-syntax form. Same semantics as the bare-key form. |
To list which attributes are filterable and what their option IDs are, fetch GET /api/shop/attributes and look for isFilterable: 1. Each attribute's options[] array has the IDs.
Sort tokens
The compound form (<key>-<dir>) is recommended:
| Token | Sort by |
|---|---|
name-asc | Product name, A→Z |
name-desc | Product name, Z→A |
price-asc | Price, low→high |
price-desc | Price, high→low |
created_at-asc | Oldest first |
created_at-desc | Newest first |
updated_at-asc | Oldest update first |
updated_at-desc | Most recently updated first |
id-asc | Ascending ID (catalog order) |
id-desc | Descending ID |
Alternative form: ?sort=name&order=desc. Pick one; mixing them isn't useful.
Combining filters
All filters are AND-combined. There is no OR across different filters — the only multi-value form is comma-separated values within a single attribute (?size=4,5,6).
GET /api/shop/products?
category_id=5
&color=3
&size=4,5
&price=10,200
&new=1
&sort=price-desc
&per_page=24Response
200 OK — JSON array of card-level product objects (same shape as Products). Pagination headers always emitted.
A filter that matches nothing returns an empty array with 200 OK and X-Total-Count: 0. It does not 404.
Common pitfalls
- Sending
itemsPerPageinstead ofper_page— the legacy API Platform name is not accepted. - Filtering by an attribute that isn't flagged
isFilterable=1— the parameter is silently ignored. - Filtering by an attribute code but passing an option label (e.g.
?color=Red) — values are matched against option IDs, not labels. - Reading the comma in
priceas a thousands separator — it isn't.?price=10,200is min 10 / max 200, not "10200". Never group digits (?price=1,000is invalid); write?price=1000,5000. - Combining
price+price_from/price_to— pick one. The compound form wins if both are present. - Assuming an unknown
sorttoken errors — it does not.?sort=bogus-descanswers200with the default catalog order, so a typo looks like "sorting is broken" rather than a rejected request.
Related Resources
- Products — same endpoint with no filters
- Single Product — fetch one product after the user picks a result
- Categories — discover category IDs for
?category_id=N - Attributes — discover filterable attribute codes
- Pagination — the page, per_page, and header conventions used here

