Emergency situation

In case of emergencies or breakdowns, you can send an SMS to our emergency hotline

On-call phone (SMS only)

+45 29 70 15 95

Send an SMS with the following information:

  • Your name and webshop
  • Description of the problem
  • Your callback phone number

Notes: This service is only for critical situations where your webshop is down or has serious problems. For regular support, please use our normal support channels.

Ajax filtering

Technical documentation for Shoporama's /ajax endpoint for filtering products. For developers and theme designers.

Reading time: approx. {eight} minutes
Shopejer Developer

All Shoporama stores have a built-in /ajax endpoint that returns products in JSON format. This makes it possible to implement dynamic filtering, lazy loading, and infinite scrolling without reloading the page, providing customers with a fast and modern experience.

This article is primarily written for developers and those who are building or customizing their own theme. If you’re using the Delaware theme, you already have AJAX filtering out of the box and don’t need to call the endpoint yourself.

How to call the endpoint

A simple call that fetches products from one or more categories:

fetch('/ajax?categories=123&limit=24')
  .then(response => response.json())
  .then(products => {
    products.forEach(p => console.log(p.name, p.price));
  });

By default, the endpoint returns a JSON array of products. If you also want metadata and pagination, add `include_meta=1 ` and `include_pagination=1`. You’ll then receive an object containing ` products`, `meta`, and `pagination`.

Available Parameters

Several of the parameters accept pipe-separated values, allowing you to filter by multiple criteria at once. For example, ` categories=12|34|56`.

ParameterDescription
categoriesCategory IDs, pipe-separated. If multiple IDs are specified, products in at least one of the categories are returned (OR). Add match=all to return only products in all of the specified categories (AND)
force_categoriesSame as ` categories`, but enforces the categories without allowing other filters to further narrow them down
price_rangePrice range as min|max, e.g., 100|500
attribute_valuesIDs of attribute values (e.g., color, size), separated by pipes. Multiple values are matched as OR. Add match=all to require that the product has all the specified values (AND), e.g., both a color and a size
attribute_tagsFilter attribute values by their tag instead of their ID, e.g., red|blue
attribute_tags_in_stockSame as attribute_tags, but only variants that are in stock
attribute_tagFilter by an entire attribute (not a specific value) based on its tag, e.g., only products that have the "color" attribute
shared_valuesIDs of shared values, e.g., everything in Medium regardless of profile. | means “or,” and , means “and,” e.g. , 2|3.5. See the section on shared values further down
shared_values_in_stockSame as ` shared_values`, but only variants in stock with the value
extension[id]Filter by custom fields. id is the custom field ID, and the value can be pipe-separated
brandsBrand IDs, pipe-separated
suppliersSupplier IDs, pipe-separated
landing_pagesLanding page IDs, pipe-separated
product_idsSpecific product IDs, pipe-separated
tagsFilter by product tags
sort + sort_orderSort the results. sort_order is asc or desc (default)
limit / offsetPagination. limit can be at most 1000; higher values are automatically truncated
metaSpecific meta field to include on each product. Use _all for all meta fields, or separate multiple names with pipes
only_in_stock_variantsReturn only variants that are in stock, as indicated in the variant_stock field for each product
include_metaSet to 1 to include attributes, categories, and brands in the results (useful for building a filter UI)
include_paginationSet to 1 to include offset, limit, count, and total
prettySet to 1 for nicely formatted JSON (good for debugging)
rebuildSet to 1 to force a new cache generation for that specific URL

Filtering with AND (match=all)

When you send multiple categories or attribute_values, the endpoint matches by default using OR: a product is included if it matches at least one of the values. This works well for broad overviews, but for faceted filtering, you’ll often want the opposite—namely, only those products that match all the selected values at once.

Add `match=all` to switch to AND logic. Then, the product must have every single one of the specified values to be included. This is exactly what you need when a customer selects both a color and a size in your filter:

// Only products that have BOTH value 401815 AND 401822
fetch('/ajax?attribute_values=401815|401822&match=all&limit=50')
  .then(response => response.json())
  .then(products => { /* ... */ });

`match=all` applies to both `categories ` and ` attribute_values`. Without this parameter, `OR` is used as before.

Note: The older `exclude=1` parameter does exactly the same thing as `match=all` and still works for backward compatibility. The name is misleading, as it does not exclude anything, so use `match=all` in new code.

Note: The parameters `category_id`, `tag`, ` extra_field[..]`, `price_from `, and `price_to ` no longer exist. Use `categories`, `tags`, `extension[id] `, and `price_range` instead. Incorrect names will simply return 0 results or be ignored; no error message will be displayed.

Filtering by Shared Values

If the store uses shared values, you can filter across profiles. A shared value such as “Medium” can be linked to a value in both the T-shirt and pants profiles, and `shared_values` finds products with all of them without requiring you to know the individual attribute value IDs. Both variants and regular attribute values in the product’s current profile are included.

The parameter uses the same structure as atags: | means “or” within a group, and , separates groups that must all match. If Medium is ID 2, Large is ID 3, and 400 bar is ID 5, then `shared_values=2|3,5 ` returns products that are available in Medium or Large and also have 400 bar. An invalid ID returns no products—not the entire catalog.

// Everything in Medium or Large, across profiles
fetch('/ajax?shared_values=' + encodeURIComponent('2|3') + '&include_meta=1&limit=24')
  .then(r => r.json())
  .then(data => {
    console.log(data.products);
    console.log(data.meta.shared_values);
  });

If you use `shared_values_in_stock` instead, only variants that are in stock will include that value. For regular attribute values (not variants), the product itself must be in stock or allow negative inventory. If you send both parameters, ` shared_values_in_stock` is used.

With `include_meta=1`, the response also includes `meta.shared_values`: the shared values found on the products in the results, sorted in the same order as under “Shared Values” in the admin panel. Each entry has ` attribute_shared_value_id`, `value`, `num_value`, `weight`, and `attribute_value_ids ` (the profile values in the results that are linked to the shared value). At the same time, each value in the ` meta.attributes ` field has an `attribute_shared_value_id`, which is ` null ` for values that are not shared. This allows you to build one filter entry per shared value instead of one per profile value.

`num_value` is the number at the beginning of the value, e.g., 400 for “400 bar.” The webshop’s own filter doesn’t use it, but you can use it yourself in your theme, e.g., for sorting.

In your theme, you can also retrieve the list of the store’s shared values using $webshop->getSharedValues(), which returns the id, value, and num_value for each one.

Example: Category, price range, and color

Retrieve red products priced between 100 and 500 kr. from two categories, sorted by price in ascending order:

const params = new URLSearchParams({
  categories: '12|34',
  price_range: '100|500',
  attribute_tags: 'red',
  sort: 'price',
  sort_order: 'asc',
  limit: 24
});

fetch('/ajax?' + params)
  .then(r => r.json())
  .then(products => renderProducts(products));

Example: Filter using extension fields and retrieve metadata

Extension fields are specified by the extension field’s ID in curly braces. You can find the ID under Settings → Extended Fields in the admin panel. Example where extension field 5 (e.g., "material") must be either "cotton" or "linen":

// extension[5]=cotton|linen
fetch('/ajax?categories=12&extension[5]=' + encodeURIComponent('cotton|linen') + '&include_meta=1&include_pagination=1&limit=24')
  .then(r => r.json())
  .then(data => {
    console.log(data.products);
    console.log(data.meta.attributes);
    console.log(data.pagination);
  });

Fields for each product in the response

Each product in the `products` array has the following fields, among others:

JSON-svar fra /ajax-endpointet med produktfelter som product_id, own_id, name, price, price_dk, stock, og variant_stock med lagervarianter
The JSON response from the /ajax endpoint on mortensbutik.dk. Each product has fields such as product_id, name, price, price_dk, stock, and variant_stock, which shows stock levels per variant.
  • product_id, own_id, name, description, list_description
  • price, real_price, sale_price, price_dk (formatted for Danish)
  • stock, attr_stock, variant_stock, stock_string_da
  • brand_name, supplier_id, supplier_name, profile_name
  • category_ids, category_names
  • thumbnail (200x200), thumbnails (array of all images at 200x200), url
  • avg_rating, online_since, delivery_time, delivery_time_not_in_stock, approx_shipping
  • has_campaigns, campaign_info
  • meta_values (filled in only if you send meta=...)

If the online store has enabled “hide stock via AJAX,” stock, attr_stock, and stock quantities in variant_stock will be null to prevent stock levels from being publicly disclosed.

Caching

The endpoint is cached on the server for 12 hours per unique URL. The response is also sent with correct Last-Modified and Expires headers, so that browsers and intermediate caches can return a quick 304 Not Modified if the content has not changed. This ensures fast response times, but it also means that changes to a product won’t take effect until the cache expires. You can force a cache rebuild for a specific URL by adding `rebuild=1`.

Read more in the article “Caching in Shoporama,” which provides an overview of the cache layers in general.

Implementation in Your Theme

To build a complete AJAX-filtered product list, the developer typically needs to:

  1. Build a filter UI with checkboxes or dropdowns based on ` meta.attributes`, ` meta.brands `, and `meta.categories`
  2. Listen for changes to the filters and combine them into a query string
  3. Call /ajax with the selected parameters
  4. Dynamically update the product list and display pagination based on `pagination.total`

Tip: Read more about filtering in general in Filtering on Your Webshop. If you’re using the Delaware theme, you already have AJAX filtering out of the box.

Frequently Asked Questions

Where can I find the ID of a custom field or an attribute value?

In the admin panel under Settings → Extended Fields for custom fields, and under Web Store → Product Profiles for attribute values. The IDs are displayed in the list or in the URL when you edit a field.

Where can I find the ID of a shared value?

The IDs are not displayed on the Shared Values page in the admin panel. Instead, retrieve them in the theme using $webshop->getSharedValues(), from meta.shared_values in an /ajax call with include_meta=1, or from the REST endpoint GET /attribute-shared-value.

Why do I get no results when I use category_id=123?

Because the parameter doesn’t exist. Change it to categories=123 (plural). This is one of the most common mistakes when setting up AJAX filtering for the first time. Also, make sure you aren’t usingprice_from/price_to or extra_field[...], as those don’t exist either.

My changes to a product aren’t showing up on /ajax. What should I do?

The endpoint is cached for 12 hours. Wait, or retrieve the URL with &rebuild=1 to force a regeneration of that specific URL.

Do lots of AJAX calls affect my website’s speed? (Mikkel, developer)

The cache ensures that repeated calls are fast. However, be careful not to make a new call for every single keystroke in a search field. Use "debounce" so that you only make the call after the user pauses for 200–300 ms. The browser also utilizes If-Modified-Since, so unchanged responses return as 304 and use almost no bandwidth.

Will I get the same result as on the category page?

Pretty much. /ajax follows the same rules that ProductFactory uses on category pages, so filtering, sorting, and visibility (e.g., hidden products) behave the same way.

Can I perform a search on top of /ajax? (Sofie, new hire)

/ajax does not accept a free-text search parameter. For searching, you must use Shoporama’s dedicated search endpoint in the theme (typically /search) or filter by product_ids if you’re handling the search yourself and simply want to retrieve data for a known list of products.

How many products can I retrieve in a single request? (Jonas, Scaling)

You can retrieve up to 1,000 products in a single call. If you request more, the response is automatically capped at 1,000, and the shop will simultaneously receive a notification in the admin panel under “Messages” so you can see it. In practice, you should aim for a somewhat lower number. A few hundred per call keeps the JSON payload small and the response fast, which is especially important for mobile users. Use `limit` and `offset` to paginate, or only fetch the next batch when the user scrolls.

Will my customers’ inventory levels be publicly displayed? (Malene, Marketing)

By default, the response includes stock levels. If you want to hide them (so competitors or bots can’t see how much you have in stock), your developer can enable “hide stock via AJAX” on the online store, after which the stock fields will be sent as null.

Can I use /ajax as a “real” REST API? (Mikkel, developer)

No, it’s a public, cache-friendly product endpoint for front-end use. If you need to create, update, or integrate at a deeper level, use the actual REST API instead, which requires an API key.

Should I be concerned that filtering works differently for customers in different languages?

The endpoint runs in the same webshop context as the homepage, so prices, currency, and visibility follow the webshop’s settings. If you have multiple webshops or languages, remember that each webshop has its own URL and, therefore, its own /ajax-cache.

Would you like us to help you integrate AJAX filtering into your theme, or do you have technical questions? Email us at support@shoporama.dk.