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 to include metadata and pagination, add `include_meta=1 ` and `include_pagination=1`. You’ll then receive an object containing ` products`, `meta`, and `pagination` instead.

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, separated by pipes. 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 specified categories (AND)
force_categoriesSame as categories, but enforces the categories without allowing other filters to narrow them down further
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 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 pipe-separate multiple names
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
include_similarSet to 1 to include the product’s similar products (e.g., other colors) and the product’s own attributes on each product. See the section on similar products below
prettySet to 1 for nicely formatted JSON (good for debugging)
rebuildSet to 1 to force a new cache generation for the 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; they do not trigger an error message.

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 be included. For regular attribute values (not variants), the product itself must be in stock or allow negative stock. 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 result, 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 create one filter condition 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 parentheses. 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);
  });

Similar Products and Color Variants

If each color is listed as its own product and the colors are linked under "Similar Products," you can display the other colors directly on the product card, for example as small color dots. Add `include_similar=1`, and each product will have two additional fields:

  • similar_products: the similar products with product_id, name, url, thumbnail (200x200 fit), in_stock (true or false), and attributes. Only products that are online are included, with a maximum of 25 per product, in the order they appear on the product page in the admin panel. The product itself is not included.
  • attributes: the product’s own attributes, so you also know the card’s own color. Each attribute has attribute_id, attribute_name, attribute_tag, purpose, attribute_value_id, value, and value_tag.

The attributes are those assigned to the product. Variants (e.g., sizes) and fields that the customer fills out themselves are not included. "purpose" is the purpose the attribute serves in the profile (e.g., "color" when "Purpose" is set to "Color"), and "attribute_tag " and "value_tag" are the tags you’ve assigned to the attribute and its value. For example, add the hex code as a tag to each color value, and the theme can draw the dot directly. A text attribute containing the hex code also works. In that case, the code is stored in ` value`.

"attributes": [
  {"attribute_id": 120, "attribute_name": "Color",
   "attribute_tag": "color", "purpose": "color",
   "attribute_value_id": 801, "value": "White",
   "value_tag": "#ffffff"}
],
"similar_products": [
  {
    "product_id": 1043,
    "name": "Original T-shirt | Black",
    "url": "https://example.com/original-t-shirt-sort",
    "thumbnail": "https://example.com/cache/.../fit-200x200x90.png",
    "in_stock": true,
    "attributes": [
      {"attribute_id": 120, "attribute_name": "Color",
       "attribute_tag": "color", "purpose": "color",
       "attribute_value_id": 802, "value": "Black",
       "value_tag": "#000000"}
    ]
  }
]
// Color dots on product cards: the card's own color first, then the others
const color = attributes => attributes.find(a => a.purpose === 'color');

fetch('/ajax?categories=12&include_similar=1&limit=24')
  .then(r => r.json())
  .then(products => {
    products.forEach(product => {
      const own = color(product.attributes);
      const others = product.similar_products
        .map(similar => ({ url: similar.url, color: color(similar.attributes) }))
        .filter(similar => similar.color);

      console.log(product.name, own ? own.value_tag : null, others);
    });
  });

Without `include_similar`, the two fields are not included, and the result is the same as before. Similar products can also be items other than colors, such as accessories. Therefore, only display those that have a color attribute.

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 in 200x200), url
  • avg_rating, online_since, delivery_time, delivery_time_not_in_stock, approx_shipping
  • has_campaigns, campaign_info
  • meta_values (only filled in if you send meta=...)
  • attributes, similar_products (only included if you send include_similar=1)

If the online store has enabled "hide stock via AJAX," stock, attr_stock, and stock quantities in variant_stock will be null, so that stock numbers are not publicly disclosed.

Caching

The endpoint is cached on the server for 12 hours per unique URL. The response is also sent with the correct Last-Modified and Expires headers, so that browsers and intermediate caches can return a fast 304 Not Modified if the content has not changed. This ensures fast response times but 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 Webstore → 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’re not usingprice_from/price_to or extra_field[...], as those don’t exist either.

My changes to a product aren’t taking effect 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 many 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 uses the If-Modified-Since header, so unchanged responses return a 304 status 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 /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 call? (Jonas, Scale)

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’ stock levels be publicly exposed? (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.

Can I display the other colors on the product card? (Line, theme developer)

Yes, if the colors are linked under “Similar Products.” Send `include_similar=1`, and find the color in the `attributes` of the product and each product in `similar_products`—for example, where `purpose` is `color`. The hex code may be included as a tag on the color value. This doesn’t require an extra call per product, since the similar products are fetched collectively for the entire list.

Should I be concerned that the 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.