Product Catalog

Keys and channels

Every storefront call carries a publishable key (pc_pk_…) in X-Catalog-Key. A key has a type, its channel, that decides where it may be used. Create and revoke keys in the Product Catalog dashboard under Storefront › Keys.

Publishable keys are public

What a key protects, and what it does not

Publishable keys are public. A browser key ships in your page's JavaScript, and anyone can copy it. It reads only what every shopper already sees: public products, categories, collections and published reviews.

Allowed websites protect browsers only. They stop another website from using your key in its visitors' browsers. They do not stop a script or a server, which can send any Origin header, or none.

Carts are protected by server-side resolution. A storefront sends SKUs and quantities, never prices. The catalog prices every line on its side, when the cart is created and each time Throttle asks again, so nothing sent with a publishable key can change what a shopper pays.

Key types

Choose the type when you create the key. The catalog enforces it on every storefront route:

TypeRequest with an OriginRequest without an OriginUse it for
BrowserMust be on the key's allowed websites, else 403 ORIGIN_NOT_ALLOWED403 ORIGIN_NOT_ALLOWEDYour website’s browser code.
AppMust be on the key's allowed websites (refused when the list is empty)AllowedA native app or another client that is not a web page.
ServerAlways refused: 403 SERVER_KEY_IN_BROWSERAllowedServer rendering, static builds and scripts.
LegacyAny websiteAllowedKeys made before key types existed, shown as “Legacy (any website)”.

A legacy key keeps working from any website until you give it a type in Storefront › Keys; the key itself does not change. A store can have up to 20 active publishable keys. Revoking a key deletes it, and the next request with it answers 401 AUTHENTICATION_FAILED.

Allowed websites

Browser and app keys carry a list of up to 20 allowed websites. Each is an exact origin: https, a host and an optional port, with no path, such as https://shop.example.com or https://shop.example.com:8443. A subdomain or another port is a different origin and needs its own line. http: origins, including http://localhost, cannot be listed; during development, call the catalog from your dev server with a server key, or serve the site over https.

In the browser: a browser key
import { createCatalogClient } from '@usethrottle/catalog';

// In the browser: a browser key. It works only from the allowed websites
// you listed on the key, such as https://shop.example.com.
export const catalog = createCatalogClient({
  publishableKey: 'pc_pk_your_browser_key',
});

Server rendering and static builds use a server key

Pages rendered on a server (Next.js, Remix, Astro and the like), static site builds, sitemap jobs and scripts call the catalog without an Origin. Give them a server key. It is refused on any request that carries an Origin, so it cannot be lifted into a browser page, and it has the higher per-key limit with no per-visitor bucket: server traffic arrives from a few hosting addresses, which a per-visitor limit would throttle. Use the browser key in the client-side part of the same app.

On the server: a server key
import { createCatalogClient } from '@usethrottle/catalog';

// On your server, in a static build or a script: a server key. The catalog
// refuses it on any request that carries an Origin header, so it never works
// from a browser page.
export const catalog = createCatalogClient({
  publishableKey: process.env.CATALOG_SERVER_KEY ?? '',
});

A server key is still a publishable key and reads only public data. Keep it in a server environment variable anyway, so its higher limit stays yours. The Next.js page shows the full setup.

Sending the key in the URL: keyInQuery

By default every request sends the key in the X-Catalog-Key header, which makes a browser preflight each new URL. Set keyInQuery: true (1.2.0 and later) on a browser client and a read (a GET) sends ?key= instead, with no custom header, so a plain read needs no preflight.

A browser client that sends the key in the URL
import { createCatalogClient } from '@usethrottle/catalog';

// In the browser: reads send ?key= instead of the X-Catalog-Key header, so a
// plain read needs no preflight. Browser and app keys only; a POST still sends
// the header, and the customer token is never put in a URL.
export const catalog = createCatalogClient({
  publishableKey: 'pc_pk_your_browser_key',
  keyInQuery: true,
});
  • Reads only. A POST always sends the header. createServerCatalogClient ignores the option. A revalidation with If-None-Match, or any client headers, still preflights.
  • Never a server key in a URL. The catalog refuses it with 403 KEY_NOT_ALLOWED_IN_URL (an OriginNotAllowedError in the SDK). Use keyInQuery with browser and app keys only. A URL is recorded wherever full URLs are kept (CDN and zone logs, analytics, browser history), so rotate any server key that was ever sent in a URL, even though it was refused.
  • The customer token is never put in a URL, in any mode. It travels only in X-Customer-Token, so a read that carries a token still preflights.
  • Suggestions never send the customer token and, with keyInQuery on, send no custom header at all: see Search and suggestions.

Secret keys never reach a storefront

The catalog also has secret keys (pc_sk_…) for writing products from your own systems. A secret key is never for a storefront: storefront routes refuse it with 401 AUTHENTICATION_FAILED, and the SDK refuses it before any request. createCatalogClient throws CatalogError INVALID_PUBLISHABLE_KEY for anything that is not a pc_pk_ key, and for a pc_sk_ key the message says so: “Secret keys (pc_sk_) must never be used in a storefront. Use a publishable key (pc_pk_).” The key itself is never part of an error message.

Carts need the same websites in Throttle

Creating a cart goes through the catalog, but changing it from the browser (adding lines, the useCart hook, the checkout hand-off) talks to Throttle's cart-session routes directly, and Throttle checks the Origin against its own list. Add the same websites to Throttle's allowed origins in your application's settings (see Cart sessions), or cart calls from them are refused with Throttle's origin_not_allowed.

Telling the origin errors apart
import {
  createCatalogClient,
  OriginNotAllowedError,
} from '@usethrottle/catalog';

const catalog = createCatalogClient({
  publishableKey: 'pc_pk_your_browser_key',
});

try {
  await catalog.products.list({ limit: 24 });
} catch (error) {
  if (error instanceof OriginNotAllowedError) {
    // ORIGIN_NOT_ALLOWED: this website is not on the key's allowed websites.
    // SERVER_KEY_IN_BROWSER: a server key was used from a browser page.
    // origin_not_allowed (source 'throttle'): a cart call from a website
    // missing from Throttle's allowed origins.
    console.error(error.code, error.source);
  } else {
    throw error;
  }
}

Rate limits by key type

Key typePer visitor (IP)Per key, all visitors
Browser, app and legacy300 requests a minute (600 for suggestions)12,000 requests a minute
ServerNone60,000 requests a minute

Over a limit the answer is 429 RATE_LIMITED with Retry-After. Errors and rate limits covers what the SDK does with it.