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
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:
| Type | Request with an Origin | Request without an Origin | Use it for |
|---|---|---|---|
| Browser | Must be on the key's allowed websites, else 403 ORIGIN_NOT_ALLOWED | 403 ORIGIN_NOT_ALLOWED | Your website’s browser code. |
| App | Must be on the key's allowed websites (refused when the list is empty) | Allowed | A native app or another client that is not a web page. |
| Server | Always refused: 403 SERVER_KEY_IN_BROWSER | Allowed | Server rendering, static builds and scripts. |
| Legacy | Any website | Allowed | Keys 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.
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.
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.
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
POSTalways sends the header.createServerCatalogClientignores the option. A revalidation withIf-None-Match, or any clientheaders, still preflights. - Never a server key in a URL. The catalog refuses it with
403 KEY_NOT_ALLOWED_IN_URL(anOriginNotAllowedErrorin the SDK). UsekeyInQuerywith 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
keyInQueryon, 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.
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 type | Per visitor (IP) | Per key, all visitors |
|---|---|---|
| Browser, app and legacy | 300 requests a minute (600 for suggestions) | 12,000 requests a minute |
| Server | None | 60,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.