> For the complete documentation index, see [llms.txt](https://docs.abtasty.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.abtasty.com/commerce/merchandising/how-to-integrate-merchandising-via-api.md).

# How to integrate Merchandising via API

### Overview

The Merchandising feature surfaces curated product lists (strategies) configured in the AB Tasty back-office.

Two endpoints are exposed depending on what you already know about the strategy:

* **By category ID** — when your front-end only knows the current category, hit the category endpoint and the API resolves the bound strategy for you.
* **By strategy UUID** — when the UUID is already known (cached, configured, etc.), call the UUID endpoint directly.

Both endpoints return the same payload shape (items, facets, pagination) and accept the same query parameters.

Base URL: `https://uc-info.eu.abtasty.com/v1/reco/v1`

> **`v1` appears twice, and both are required.** The first belongs to the AB Tasty gateway, the second selects version 1 of the Merchandising API.
>
> A URL built with a single `v1` (`https://uc-info.eu.abtasty.com/v1/reco/{site_id}/...`) reaches a deprecated internal endpoint. It answers `200` too, so the mistake produces no error — only wrong results:
>
> `facets` and `filters[]` behave the same on both.

|                          | Single `v1` (deprecated)           | Double `v1` (this guide)             |
| ------------------------ | ---------------------------------- | ------------------------------------ |
| `merch/category_id/{id}` | returns a strategy UUID            | returns products                     |
| `sort[field]`            | silently ignored, results unsorted | applied, or `400 INVALID_SORT_FIELD` |
| `limit` omitted          | returns the **entire** list        | returns 20 items                     |
| `limit=0`                | `422`                              | no limit                             |

***

### Authentication

All requests require a JWT in the `Authorization` header:

```
Authorization: Bearer <token>
```

***

### Endpoints

#### 1. Fetch items by category ID

```
GET /{site_id}/merch/category_id/{category_id}
```

Resolves the merchandising strategy bound to `category_id` and returns its ranked products in a single call.

Use this endpoint when your front-end knows the category it is rendering but not which strategy is deployed on it. The binding lives in the AB Tasty back-office, so merchandisers can change the strategy on a category without a front-end release.

The `category_id` in the path also scopes the products returned — you do **not** need to repeat it in `variables`.

**Path parameters**

| Parameter     | Type    | Description                   |
| ------------- | ------- | ----------------------------- |
| `site_id`     | integer | Your AB Tasty site identifier |
| `category_id` | string  | The category identifier       |

Query parameters, response payload and error codes are identical to the by-UUID endpoint, with one extra error:

| Status | Error code         | Reason                                                        |
| ------ | ------------------ | ------------------------------------------------------------- |
| 400    | `UNKNOWN_CATEGORY` | No strategy is bound to this `category_id` for this `site_id` |

***

#### 2. Fetch items by strategy UUID

```
GET /{site_id}/merch/{merch_id}
```

Returns the ranked list of products for a given strategy, with optional filtering, faceting and pagination. Use this endpoint when the strategy UUID is already known.

> **This endpoint needs the category in `variables`.** A strategy can be deployed on several categories, so its UUID alone does not say which products to rank. Without `variables={"category_id":"…"}` the response is a valid `200` with an **empty** `items` array. See Scoping a strategy to a category.

**Path parameters**

| Parameter  | Type    | Description                        |
| ---------- | ------- | ---------------------------------- |
| `site_id`  | integer | Your AB Tasty site identifier      |
| `merch_id` | string  | UUID of the merchandising strategy |

**Query parameters**

| Parameter       | Type       | Default  | Description                                                                                                                                                               |
| --------------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `variables`     | JSON       | —        | JSON object injected into the strategy template. Required in practice on the UUID endpoint — see Scoping a strategy to a category. Ex: `variables={"category_id":"1439"}` |
| `fields`        | JSON array | —        | JSON array of product fields to return. Ex: `["id","title","price"]`. `id` is always included.                                                                            |
| `page`          | integer    | `null`   | Page number (1-indexed). Ignored when `offset` is also provided.                                                                                                          |
| `offset`        | integer    | `0`      | Zero-based item offset. Takes precedence over `page`.                                                                                                                     |
| `limit`         | integer    | **`20`** | Number of items per page. **Defaults to `20` when omitted.** Use `0` for no limit.                                                                                        |
| `facets`        | boolean    | `false`  | Set to `true` to include facet counts in the response.                                                                                                                    |
| `filters[f][]`  | string     | —        | Filter on field `f`. Repeat for multiple values (OR within a field, AND across fields). See filter syntax below.                                                          |
| `sort[f]`       | string     | —        | Override the strategy's ordering. `f` is the field name, value is `asc` or `desc` (case-insensitive). See sort syntax below.                                              |
| `output_format` | string     | `json`   | Response format. Accepted values: `json`, `csv`, `proximis`.                                                                                                              |
| `delimiter`     | string     | `;`      | CSV delimiter (only used when `output_format=csv`). Accepted values: `,`, `;`, `%09`, \`                                                                                  |

These query parameters apply to **both endpoints** (UUID and category).

**Scoping a strategy to a category**

Merchandising strategies rank the products of **one category**. The category is supplied through the `category_id` variable:

```
?variables={"category_id":"1439"}
```

* **Category endpoint** — supplied automatically from the path. Pass `variables` only to override it.
* **UUID endpoint** — you must supply it yourself. A strategy can be deployed on several categories, so the UUID does not imply one.

When the category is missing, the strategy matches no products and the response is a valid `200` with an empty `items` array and `total_items: 0` — not an error. An empty result on the UUID endpoint is almost always a missing `category_id`.

> The variable is named `category_id` by default, but it is configurable per site. If your strategies were set up with a different name, use that one — your AB Tasty contact can confirm which applies to your site.

**Filter syntax**

Filters use PHP-style bracket notation. Two syntaxes are supported depending on the operation needed.

**Combining rules**

* Multiple values on the **same field** → combined with **OR**
* Filters on **different fields** → combined with **AND**
* Field names must match `^[A-Za-z_][A-Za-z0-9_]*$` (letters, digits and underscores only)

***

**Syntax 1 — Equality / IN (empty index `[]`)**

Use `filters[field][]=value` for equality. Repeat the parameter on the same field to produce an `IN` (OR) filter.

| Operation | Query string                                          | Behaviour                        |
| --------- | ----------------------------------------------------- | -------------------------------- |
| Equals    | `filters[available][]=1`                              | `available = 1`                  |
| IN (OR)   | `filters[category][]=shoes&filters[category][]=boots` | `category IN ('shoes', 'boots')` |

***

**Syntax 2 — Comparison operators (indexed entries)**

Use `filters[field][{idx}][operator]` + `filters[field][{idx}][value]` for `>`, `<`, `>=`, `<=` comparisons. Use incrementing indices (`0`, `1`, …) to stack multiple conditions on the same field.

Allowed operators: `=`, `>`, `<`, `>=`, `<=`

| Operation          | Query string                                                                                                                                                               |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Greater than       | `filters[price][0][operator]=>&filters[price][0][value]=10`                                                                                                                |
| Less than or equal | `filters[price][0][operator]=<=&filters[price][0][value]=500`                                                                                                              |
| Range (min + max)  | <p><code>filters\[price]\[0]\[operator]=>=\&filters\[price]\[0]\[value]=10</code><br><code>\&filters\[price]\[1]\[operator]=<=\&filters\[price]\[1]\[value]=500</code></p> |

***

**Combined example** — in-stock shoes priced between €10 and €500:

```
filters[available][]=1
&filters[category][]=shoes&filters[category][]=boots
&filters[price][0][operator]=>=&filters[price][0][value]=10
&filters[price][1][operator]=<=&filters[price][1][value]=500
```

**Sort syntax**

The `sort[field]=direction` parameter **replaces the strategy's own ordering** (pins included). The strategy's catalog and your active `filters[...]` still apply.

```
?sort[price]=desc
?sort[created_at]=asc
```

* Only **one** sort field per request.
* Direction is `asc` or `desc`, case-insensitive.
* The field must be flagged **`merch_sortable`** for your site in the AB Tasty back-office (this is a dedicated flag, distinct from the internal `sortable` flag). Requesting an unmarked field returns `400 INVALID_SORT_FIELD`.

**Sort-specific error codes** (all returned as `400 Bad Request`)

| Error code               | Reason                                                 |
| ------------------------ | ------------------------------------------------------ |
| `INVALID_SORT_SYNTAX`    | Malformed `sort[...]` key, or more than one sort entry |
| `INVALID_SORT_FIELD`     | Field is not sortable for this site                    |
| `INVALID_SORT_DIRECTION` | Direction is not `asc` / `desc`                        |
| `SORT_UNSUPPORTED`       | The strategy does not support runtime sorting          |

**Response (JSON)**

```json
{
  "name": "strategy-name",
  "items": [
    {
      "id": "product-123",
      "title": "Running Shoes",
      "price": 89.99
    }
  ],
  "total_items": 248,
  "total_pages": 13,
  "current_page": 1,
  "limit": 20,
  "has_next": true,
  "facets": {
    "category": {
      "shoes": 84,
      "boots": 40
    },
    "available": {
      "1": 248
    }
  }
}
```

**Response fields**

| Field          | Type    | Description                                                         |
| -------------- | ------- | ------------------------------------------------------------------- |
| `name`         | string  | Internal name of the merchandising strategy                         |
| `items`        | array   | Ordered list of products for the requested page                     |
| `total_items`  | integer | Total number of products matching the strategy and active filters   |
| `total_pages`  | integer | Total number of pages given the current `limit`                     |
| `current_page` | integer | Current page number (1-indexed)                                     |
| `limit`        | integer | Number of items per page used for this response                     |
| `has_next`     | boolean | Whether a next page exists                                          |
| `facets`       | object  | Facet counts by field and value. Only populated when `facets=true`. |

**Response statuses**

| Response Status | Message              | Reasons                                                                                   |
| --------------- | -------------------- | ----------------------------------------------------------------------------------------- |
| 200             | OK                   |                                                                                           |
| 400             | Bad Request          | Strategy not found for this `merch_id`; invalid filter field; query execution error       |
| 403             | Forbidden            | Missing token, invalid/expired token, or `site_id` mismatch                               |
| 422             | Unprocessable Entity | `variables` is not valid JSON; `fields` is not a valid JSON list; unknown `output_format` |
| 503             | Service Unavailable  | Request exceeded the 10-second global timeout                                             |

***

### Typical integration flow

Pick one of the two entry points depending on what your front-end has on hand.

**A. You only know the category ID** — single call, the category scopes the products:

```
GET /{site_id}/merch/category_id/1439?fields=["id","title","price"]&limit=20&facets=true
→ items[], facets{}, total_items, total_pages
```

**B. You already have the strategy UUID** — single call, pass the category in `variables`:

```
GET /{site_id}/merch/{uuid}?variables={"category_id":"1439"}&fields=["id","title","price"]&limit=20&facets=true
→ items[], facets{}, total_items, total_pages
```

Query-string values containing JSON or brackets must be URL-encoded.

**Then, for both:**

```
1. Display facets and let the user apply filters
   GET ...&filters[category][]=shoes&filters[available][]=1
   → filtered items[] and updated facets{}

2. Paginate
   GET ...&page=2
   — or —
   GET ...&offset=20&limit=20
```

***

### Notes

* The `id` field is always returned regardless of the `fields` parameter.
* When both `offset` and `page` are provided, `offset` takes precedence.
* **`limit` defaults to `20` when omitted.** Pass `limit=0` to return all matching items with no pagination cap.
* Facet computation adds overhead — only request `facets=true` when you need to render facet counts.
* **Per-field opt-in is required in the AB Tasty back-office:**
  * `filters` and `facets` only accept fields flagged as **`facetable`**.
  * `sort` only accepts fields flagged as **`merch_sortable`** (distinct from the internal `sortable` flag — they are not interchangeable). Requesting a non-opted-in field returns `400`.
* Two situations look similar but are distinct:
  * **No strategy bound to the category** → `400 UNKNOWN_CATEGORY` (never a `200` with an empty payload).
  * **Strategy found but no category supplied** (UUID endpoint without `variables`) → `200` with an empty `items` array.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.abtasty.com/commerce/merchandising/how-to-integrate-merchandising-via-api.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
