> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wristler.eu/llms.txt
> Use this file to discover all available pages before exploring further.

# The catalogue

> What is for sale, from every seller — the same set the website shows.

```
GET /v1/watches
GET /v1/watches/{id}
```

Scope `catalogue:read`. The app's overview screen, and the same set the website
shows: what is actually for sale, from everybody. Whose it is does not come into
it.

Nobody has to be signed in: a guest token carries this scope, so the app can show
the catalogue before there is an account. See *Before anybody signs in* in the
build guide.

## Not the same question as an inventory

| | |
| - | - |
| `/v1/watches/{id}` | Is this for sale? |
| `/v1/sellers/{id}/watches/{id}` | What does this business have? |

A listing can be in the second and not the first — a draft, something sold,
something in review — which is exactly why they are two addresses rather than one
with a filter. The same id means a different thing depending on which you ask, and
the URL is what says which question was asked.

A listing that is not for sale — or is, at a shop that has closed or cannot take
payments yet — is left out of the list here, even for the seller who owns it. Asked for
by id it answers `404` to anybody who does not act for its seller; the seller still
reads it — under its own address is where it belongs.

## Querying

The same shape as an inventory: `page[after]` and `page[limit]` (or `page[number]` and
`page[size]`), `include=seller,watchModel,cover,images`, `fields[watches]`, `sort`
(`price`, `createdAt`, `updatedAt`; `year` in a search only), and the filters
`filter[reference]`, `filter[seller]` and `filter[externalIdentifier]`. Words and
facets — `filter[q]`, `filter[brand]`, `filter[price]` and the rest — are the
[search](https://docs.wristler.eu/app/search.md).

**For a list of cards, include `cover` rather than `images`.** A card draws one photo, and
`images` sends the whole gallery: a page of 24 listings carried 79 uploads with five
variant URLs each, most of the answer, to draw 24 thumbnails. `cover` is the first of
`images` and cannot drift from it.

```http theme={null}
GET /v1/watches?filter[reference]=126610LN&include=cover&page[limit]=25
```

On the detail page, where the gallery is the point, ask for `images`.

**And read `translatedDescription` rather than picking a language out of
`descriptionTranslations`.** It is the description in the language your `Accept-Language`
asked for, falling back to the one it was written in, and redacted the same way
`description` is. The eight-language map is no longer sent unless you name it in
`fields[watches]` — it was 71 KB against 9 KB for one language on the longest description
we have.

```http theme={null}
GET /v1/watches/223897?include=images,watchModel,seller
Accept-Language: nl
```

> [!NOTE]
>
> A seller's own identifiers — `externalIdentifier`, `dealerProductCode` — never come
> back on this address, not even to the seller they belong to: read them under
> `/v1/sellers/{id}/watches`. The description, and every translation of it, has its
> email addresses redacted for anyone but the seller.

## The shop directory

`/v1/sellers` means two things, and which one you get depends on who is asking. For a
partner it is the businesses that granted them access. For the app it is the directory
the website shows: every shop that is open, by name.

```http theme={null}
GET /v1/sellers?filter[q]=mertens&filter[country]=NL
```

The answer carries the country counts a filter panel is drawn from, and `sort` takes
`name` or `-name`:

```json theme={null}
{
  "meta": {
    "facets": {
      "country": {
        "name": "Country",
        "options": [
          { "value": "NL", "label": "Netherlands", "count": 118 },
          { "value": "DE", "label": "Germany", "count": 64 }
        ]
      }
    }
  }
}
```

The counts are worked out with the country filter left off, on purpose: picking a
country must not empty the panel it was picked in.

A guest token is enough — the directory is a page on the website anybody can open.
`logo` is the one picture a card is drawn around, so a list of shops does not need a
second call per row to find it.

> [!NOTE]
>
> A shop that has closed or never finished signing up is not in the directory: nobody
> can be sent to it. `/v1/sellers/{id}` answers `404` for one too — and so does its
> `filter[slug]` — to anybody who does not act for it, as the website sends a visitor
> of a closed shop back to the directory.

## Sharing a listing

`webUrl` on a listing is its page on the website, with the slug and in the language the request asked
for — the brand and the reference are in that slug, which is what somebody pasting the link into a
chat actually sees. Send that rather than building `…/watches/{id}` and leaning on our redirect.

A shop carries one too. Both are `null` when there is no page worth sending anybody to: a listing
that has been deleted, or a business that is not active.

## Links into the app

Once switched on, `/.well-known/apple-app-site-association` lets the iOS app open these
website pages, in every language (English has no language segment, Dutch is `/nl/horloges/…`,
German `/de/uhren/…`):

| Path | What it is | Open it with |
| - | - | - |
| `/{watches}/{id}-{name}` (the slug starts with a digit) | A listing | `GET /v1/watches/{id}`: the digits before the first `-` |
| `/{watches}/{slug}` (any other slug) | A filter page, a brand's page among them | `GET /v1/filter-pages?filter[slug]={slug}` |
| `/{watches}` or `/{watches}/`, with or without a query string | The catalogue, and a search | `GET /v1/watches`, translated as below |
| `/{sellers}/{slug}` | A shop | `GET /v1/sellers?filter[slug]={slug}` |

Two pages under the catalogue's prefix are neither and stay in the browser: the advanced
search form and the "no results" page (`/watches/advanced-search`, `/watches/no-results`,
translated per language). Checkout, account pages and everything else stay in the browser too.

Android has no path list in its file (`/.well-known/assetlinks.json`); the app's own intent
filter decides, and should claim the same paths.

### A search link

The website's search is the catalogue's address with the search in its query string. To ask
the API the same thing:

| Website | API |
| - | - |
| `query=rolex submariner` | `filter[q]=rolex submariner` |
| `filters[brand][]=Rolex`, or `filters[brand][0]=Rolex` | `filter[brand][]=Rolex` — every facet the same way, `filters[attributes.dial][]=BLUE` too. The website writes both: the advanced search numbers its lists |
| `filters[price][min]=5000`, `filters[price][max]=10000` | `filter[price][min]=500000`, `filter[price][max]=1000000` — the website counts in euros, the API in cents, so times a hundred. Either end may be missing |
| `sort_by=relevance`, or none | no `sort` |
| `sort_by=created_at:desc` | `sort=-createdAt` |
| `sort_by=price:asc` / `price:desc` | `sort=price` / `sort=-price` |
| `sort_by=attributes.year_of_production:asc` / `:desc` | `sort=year` / `sort=-year` |

Anything else in the query string (`page`, `utm_*`) can be ignored.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.