Developers

Public API

Reference of the read-only HTTP API that comparison websites use to show brands, products, merchants and prices.

The public API serves a project's brands, products, merchants and prices to a comparison website. It is read-only apart from one batch endpoint for click reports. Every website of a project has its own token and sees the project through its own languages, terms and price settings. Register websites in Websites.

All routes live under /api/public/v1 on the platform's origin.

Authentication

Send the website token as a bearer token:

curl -H "Authorization: Bearer pwt_xxxxxxxx" \
  "https://platform.example/api/public/v1/website"
  • The token (pwt_…) identifies the website. It is shown once when the website is created or its token rotated. The platform stores only a SHA-256 hash.
  • The token is a server-side secret. Call the API from the website's server (SSR, server routes, a Worker), never from the browser. The API sends no CORS headers.
  • After a rotation the previous token is accepted for 24 hours. A deleted website's token stops working immediately.
  • A missing or invalid token answers 401 with data.code set to unauthorized and a WWW-Authenticate: Bearer header.

Responses carry Cache-Control: private, max-age=60, Vary: Authorization and X-Robots-Tag: noindex, so a shared cache never serves one website's data to another.

Conventions

TopicRule
PricesInteger cents with currency: { "amountCents": 7980, "currency": "NOK" }. All prices are in the project's base currency. An offer carries originalPrice when the merchant prices in another currency, otherwise null.
DatesISO 8601 strings. Price history days are YYYY-MM-DD (UTC).
IdentifiersPublic identifiers are slugs. The only opaque id is the offer id inside outboundUrl.
LocaleEvery GET route accepts ?locale=. The response is served in the website language that best matches it: the exact locale, then its language (nb-NO to nb), then a regional variant (nb to nb-NO), then the website's default language. An unsupported locale is not an error.
PaginationList routes take page (default 1) and pageSize (default 24, at most 100) and return { items, total, page, pageSize }.
ErrorsJSON with statusCode, statusMessage, message and data. data.code is unauthorized or not_found where set.

What is served

Only live data is served:

  • An offer is shown when it is active, has a price, and its merchant is not deprecated. With the website's Hide zero prices on (default), a price of 0 counts as no price.
  • A product is listed when it has a live offer. Brands and merchants are listed when they have at least one such product.
  • Deprecated brands, products and merchants are hidden. Their slug answers 404 with data.replacedBySlug when they were merged into another, so the site can redirect permanently.
  • Custom fields are served only when marked Public.
  • Notes, scrape settings, raw results, internal ids and merchant URLs are never served.

Products carry name (without brand) and displayName in the project's product name format (default {brand.name} {product.name}). Use displayName in titles, headings and links.

SEO fields

Brand, merchant and product detail responses carry seo: { title, description, noindex }: the entity's own SEO texts, else the project's SEO template, else null. Titles do not include the website's title template. noindex entities should render robots: noindex and are left out of /sitemap.

Endpoints

GET /website

The calling website and catalogue totals. Parameters: locale.

Returns id, name, fqdn, the served locale, locales (default first), currency, pricesIncludeVat, terms (product, brand, merchant), translations (free texts), seo (titleTemplate, homeTitle, homeDescription, ogImage) and counts (brands, products, merchants, offers). Terms, texts and SEO come from Website content and fall back to the default language.

GET /brands and GET /brands/:slug

ParameterValues
locale, page, pageSize, qAs above. q filters by name (contains).
sortname (default), productCount, minPrice
orderasc (default), desc

List items: slug, name, productCount, minPrice. The detail adds description, seo, attributes, merchantCount and offerCount.

GET /products and GET /products/:slug

ParameterValues
locale, page, pageSize, qAs above.
brandBrand slug.
merchantMerchant slug: only products this merchant sells.
sortname (default), minPrice, offerCount
orderasc (default), desc

List items: slug, name, displayName, brand (slug, name), attributes, minPrice, offerCount. The detail adds description, seo, updatedAt and offers, cheapest first. Each offer has id, merchant (slug, name, websiteHost), price, originalPrice, inStock (null when the merchant does not report stock), lastCheckedAt, isCheapest, outboundUrl and attributes.

Use outboundUrl as the link target of the buy button, with rel="nofollow sponsored". See Click tracking.

curl -H "Authorization: Bearer $LISTINGS_TOKEN" \
  "https://platform.example/api/public/v1/products?brand=xqs&sort=minPrice&pageSize=12&locale=nb-NO"

GET /products/:slug/similar

Products of the same brand first, then the closest in price.

ParameterValues
localeAs above.
limit1 to 24, default 8

Returns an array of product list items.

GET /products/:slug/price-history

ParameterValues
days1 to 365, default 90

Returns { currency, days, points }. Each point is { date, minCents, offerCount }: the lowest price across the product's active offers on that day, oldest first. Prices carry forward between changes, and days without any price are left out.

GET /merchants and GET /merchants/:slug

ParameterValues
locale, page, pageSize, qAs above.
sortavgPrice (default), name, productCount
orderasc (default), desc

List items: slug, name, websiteHost, productCount, avgPrice. The detail adds description, seo, attributes and minPrice.

ParameterValues
qRequired, 1 to 200 characters.
typesComma separated list of product, brand, merchant. Default: all.
limit1 to 50, default 10

Returns { q, hits }. A hit has type, slug, name, displayName and, for products, brand. Search matches the default-language names.

GET /sitemap

Every indexable brand, merchant and product with its last change: { items: [{ type, slug, updatedAt }] }. Entities marked noindex are left out. @product-comparison/catalog-kit has buildSitemapXml to turn it into XML.

GET /go/:offerId

For self-hosted redirect clients. Query: s (the signature from the link). Returns { target, kind, trackable, ttl }. It never records a click. See Click tracking.

POST /clicks

For self-hosted redirect clients. Reports clicks in batches. See Click tracking.

Example flow

A product page needs the website settings once and the product on each request:

# Website settings: languages, terms, texts, SEO, currency
curl -H "Authorization: Bearer $LISTINGS_TOKEN" "$BASE/api/public/v1/website?locale=nb-NO"

# The product with its offers
curl -H "Authorization: Bearer $LISTINGS_TOKEN" "$BASE/api/public/v1/products/ettan-loos?locale=nb-NO"

A 404 with data.replacedBySlug means the product was merged: answer with a permanent redirect to /products/<replacedBySlug>.

Instead of writing HTTP calls yourself, use the typed listings client.
Copyright © 2026