Developers

Listings client

Use the typed @product-comparison/listings-client package to call the public API from a website's server code.

@product-comparison/listings-client is the typed client and contract of the public API. It wraps every route, serialises query arguments and rejects with a typed error. The platform imports the same package to validate requests, so the types always match what is served.

The package is private to this workspace ("private": true, version 0.0.1) and is not published to npm. Consume it as a workspace dependency ("@product-comparison/listings-client": "workspace:*") from an app in this repository, such as apps/frontend.

Entry points

ImportContents
@product-comparison/listings-clientcreateListingsClient, ListingsError, isListingsError, the response types (PublicProduct, PublicOffer, Page<T>, Money and more), the query types and the outbound helpers (isBotUserAgent, referrerHostOf, OUTBOUND_REDIRECT_HEADERS).
@product-comparison/listings-client/schemaThe zod schemas of every query string, for example productListQuerySchema, publicSearchQuerySchema and clickReportSchema.

Create a client

The token is a server-side secret. Create the client in server code only.

// server/utils/listings.ts (a Nuxt site)
import { createListingsClient } from '@product-comparison/listings-client'

export const listings = () => createListingsClient({
  baseURL: useRuntimeConfig().listingsBaseUrl, // https://platform.example
  token: useRuntimeConfig().listingsToken, // NUXT_LISTINGS_TOKEN
  locale: 'nb-NO',
})

Options

OptionDescription
baseURLPlatform origin, for example https://platform.example. The client adds /api/public/v1.
tokenThe website's pwt_… token.
localeDefault ?locale for every GET request. A call's own locale wins.
fetchA custom fetch, for tests or Workers service bindings.
userAgentOverrides the default product-comparison/listings-client.

Methods

MethodRouteReturns
website(query?)GET /websitePublicWebsite
brands.list(query?)GET /brandsPage<PublicBrandListItem>
brands.get(slug, query?)GET /brands/:slugPublicBrand
products.list(query?)GET /productsPage<PublicProductListItem>
products.get(slug, query?)GET /products/:slugPublicProduct
products.similar(slug, query?)GET /products/:slug/similarPublicProductListItem[]
products.priceHistory(slug, query?)GET /products/:slug/price-historyPublicPriceHistory
merchants.list(query?)GET /merchantsPage<PublicMerchantListItem>
merchants.get(slug, query?)GET /merchants/:slugPublicMerchant
sitemap(query?)GET /sitemapPublicSitemap
search(query)GET /searchPublicSearchResult
outbound.resolve(offerId, s)GET /go/:offerIdPublicOutboundResolution
outbound.reportClicks(clicks)POST /clicksPublicClickReportResult

search accepts either a string or { q, types, limit }. Query arguments are the partial output types of the schemas, so defaults apply when you leave a field out. The parameters are listed on the public API page.

const listings = createListingsClient({ baseURL, token, locale: 'nb-NO' })

const site = await listings.website()
const page = await listings.products.list({ brand: 'xqs', sort: 'minPrice', pageSize: 12 })
const product = await listings.products.get('ettan-loos')
const history = await listings.products.priceHistory('ettan-loos', { days: 30 })
const found = await listings.search({ q: 'tropical', types: ['product', 'brand'] })

outbound.resolve and outbound.reportClicks are meant for self-hosted redirect clients, not for rendering pages. See Click tracking.

Handle errors

Every method rejects with a ListingsError. It never contains the token.

PropertyDescription
codeunauthorized, not_found, validation, rate_limited, network or unknown. Mapped from the status: 401 and 403 to unauthorized, 404 to not_found, 400 and 422 to validation, 429 to rate_limited.
statusThe HTTP status when the server answered.
dataThe data of the error response.
replacedBySlugSet on a 404 for a product, brand or merchant that was merged into another one.
import { isListingsError } from '@product-comparison/listings-client'

export default defineEventHandler(async (event) => {
  try {
    return await listings().products.get(getRouterParam(event, 'slug')!)
  } catch (error) {
    // Merged products: redirect permanently to the surviving one.
    if (isListingsError(error) && error.replacedBySlug) {
      return sendRedirect(event, `/products/${error.replacedBySlug}`, 301)
    }
    throw error
  }
})

Use the schemas

The /schema entry exports the zod schemas the platform validates requests with. Use them to validate your own incoming query strings with the same names, defaults and limits:

import { productListQuerySchema } from '@product-comparison/listings-client/schema'

const query = productListQuerySchema.parse(getQuery(event))

It also exports MAX_CLICK_BATCH (100), MAX_CLICKS_PER_ENTRY (1000) and clickReportSchema.

  • @product-comparison/catalog-kit provides applyTitleTemplate, JSON-LD builders and buildSitemapXml for rendering what the API returns.
  • @product-comparison/redirect is a self-hostable redirect client built on this package.
Copyright © 2026