Skip to main content
Four things live here, and the first three are built the same way — and, at the end, who to ask for help. Filter pages are what the catalogue is organised into — “Rolex”, “Rolex Air-King”, “Omega under €5,000”. There are around 16,000 of them across nine languages, and they are how most people arrive. Articles are Insights: what we write, film and record. /v1/article-tags is every word they are filed under, in one call — the chips an app draws, and what filter[tag] takes. Pages are everything else: how it works, who we are, the terms, the contact page. template says which layout the website draws each one with, so an app can lay out a contact page differently from a terms page without matching on the slug. The help centre is /v1/faq-categories and /v1/faq-articles. An answer is a question and one body of HTML — not blocks, because it is written in a single editor field and a one-block list is a wrapper nobody wants to unwrap. A heading with ?include=articles is the whole screen in one call, which is why the headings are not paged. All of them need content:read, and a guest token is enough — somebody browsing before they sign up is exactly who these pages are for.

A filter page is a search with words around it

The field that matters is search. It is the listing the page stands for, written in the catalogue’s own vocabulary:
Hand it straight back to the catalogue and you have the page:
search.words goes in filter[q], search.filters are the facet filters. They are the same names the search’s own facets come back under, so a page and a filter panel speak one language — the search, in full →
That is the whole reason the page does not return its own listings. There is one way to ask “every Rolex”, and it is the one an app already implements.

Blocks

Filter pages, articles and pages carry their content as blocks: an ordered list, each with a kind and named fields.
body is HTML written in our editor. Everything else is plain text.
A block whose kind you do not recognise should be skipped rather than guessed at. The list above will grow, and an app that skips an unfamiliar kind keeps working when it does.
Fields that are empty are left out rather than sent as null, so check for a key before reading it. The one exception is a links group’s name, which is null when the links were written on the block itself rather than picked from a named list.

Why not the raw page builder

Our editor stores what it needs: content_left, list_3, image_alt. Those mean something standing next to one of our Blade templates and nothing on their own, and an app built on them would break the day somebody renames one. So each component is translated into a block, and a component kind the API has not been taught about is left out entirely — a blank space beats a half-understood guess.

In a list, blocks is null

Thousands of pages with every block loaded is the whole site in one response, and nobody draws a block in a list. Ask for the page itself and it is filled in. Filter pages come back biggest first (-watchCount); articles newest first (-publishedAt).

Autocomplete

Ranked by the same index and the same relevance threshold as the search bar on the website, so the same letters offer the same pages. Ask both — the website’s dropdown has two sections, pages and Insights, and one of them alone is half a dropdown. Articles come back newest first among what matched. A sort is refused (400, search_only_sort) while the filter pages are searching: suggestions are ordered by how well they match, and nothing else would be useful. While the articles are searching a sort is not applied — they come back newest first among what matched, whatever it says. A suggestion row is autocompleteName — the short name an editor gave the page, falling back to name — with watchCount beside it. Tapping one is a catalogue search with the search it already carries. Nothing has to be looked up in between, and there is still only one way to say “every Rolex”. A link carries a slug, so that is what they take:
Articles also filter on tag (by the tag’s slug), type — default, video or podcast, or several comma separated (podcast,video) — featured (true or 1 for the ones we are featuring, false or 0 for the rest), and author (by the author’s slug).

Who wrote it

?include=author puts the byline beside the article, as type authors: name, function, bio, linkedin, slug, image (the square portrait) and url, their page on the website. A byline, not an account — nothing else about the person is served. Everything they wrote is GET /v1/articles?filter[author]={slug}.

Personal shopper

GET /v1/personal-shopper is the account page’s “Personal shopper” block: an intro line, the shoppers (each a name and a square portrait image), and how to reach them — whatsapp and whatsappUrl, phone and phoneUrl (tel:), email. The website reads the same settings, so a new number or a new face reaches the app without a release. The website’s chat button is a live-chat widget that exists only there, and no opening hours are kept: in the app, WhatsApp is the chat. There is no filter[q] on /v1/faq-articles, and no website page to link an answer to: the help centre is the app’s own screen. It is small enough to be one call — GET /v1/faq-categories?include=articles — so search what came back, on the phone.

Ask again cheaply

Every read here carries an ETag. Send it back as If-None-Match and an unchanged page answers 304 with no body:
Content barely changes and an app re-reads the same pages on every open, over a connection it did not choose. Keep the tag beside whatever you cached, per URL — the tag covers the whole answer, so a different include or language is a different tag.
It saves the network, not the work: the answer is still built. Do not use it as a cheap way to poll for changes on something that changes often.

What else to read

Up to six articles sharing a tag, most tags in common first, in the same language, never the one being read. The same ones the website puts at the bottom of a piece — and the same shape as /v1/watches/{id}/related under a listing.

One language at a time

Every page exists as a row per locale. Accept-Language picks one, and a list without it would be the same page nine times over, so there is no way to ask for all of them.

Nothing unpublished

A page still being written, or an article scheduled for next Tuesday, is not in a list and answers 404 by id. There is no preview parameter: a draft reachable by id is a draft published.