# Quickstart: add a Shoprocket store to any site

One script tag and a publishable key. Drop in ready-made embeds, or read the catalogue over REST, draw your own shop, and let Shoprocket run the cart and checkout. Written to be followed step by step, by a developer or an AI code builder.

Web version: https://v3.shoprocket.io/docs/quickstart

## Before you start

Shoprocket is a hosted commerce backend: catalogue, cart, checkout, payments, tax, shipping and orders. You add it to a site you already have, so the site can be built with anything: plain HTML, React, Vue, Svelte, Next.js, or whatever an AI builder generates.

You need **one value: the store's publishable key** (`pk_...`). The seller finds it in the Shoprocket dashboard under **Settings > General**, in the Store details card, and every snippet on the **Embeds** page carries it too. It is public by design: it goes in browser code, and it can only read the catalogue and run a shopper's own cart.

**No Shoprocket store yet?** Create a test store for the person in one call, with no account (next section): you get its key straight away, and they claim it with a link. Or they sign up at https://app-v3.shoprocket.io/signup, create their store in the dashboard and copy the key from **Settings > General**.

You do **not** need a secret key (`sk_...`), a server, an npm package or an account with any payment provider. The store's payment setup lives in Shoprocket.

The examples on this page use a demo store in test mode (`pk_bCz5dnUr-Mdw6bj0BcoE3CALrGA1vhYq`), so every one of them runs as written and no checkout can take real money. Swap in the store's own key when you are ready.

## No Shoprocket store yet? Create a test store

When the person you are building for has no Shoprocket account, create a **test store** for them with one call. No account, no authentication. You get a working store in test mode, and a **claim link** to give the person: they sign up there and the store becomes theirs, with everything you added. Skip this section when they already have a store; ask them for its publishable key instead.

1. **Create the store:** `POST https://api-v3.shoprocket.io/v3/agent-stores` with `{"name": "..."}` (the shop's name; `baseCurrencyCode` is optional and defaults to `USD`). The answer's `data` carries `publishableKey` (`pk_...`, for the site, exactly as in the rest of this page), `storeId` (`str_...`), `secretKey` (`sk_...`), `claimUrl`, `shopUrl` and `expiresAt`.
2. **Add products** with the secret key, from your own terminal or build step: `POST https://api-v3.shoprocket.io/v3/stores/{storeId}/products` with `Authorization: Bearer sk_...`. Send `"status": "active"` (a product is a hidden draft otherwise) and its price and stock in `variants`: `[{ "price": 2400, "inventoryQuantity": 50 }]` (price in minor units; without `inventoryQuantity` it shows as sold out). Add `summary` and `description` (HTML) as you would on any shop. For a product with options, send `options` (`[{ "name": "Size", "values": ["S", "M"] }]`) and one variant per combination with `"options": { "Size": "S" }`.
3. **Images (optional):** `POST https://api-v3.shoprocket.io/v3/stores/{storeId}/media?alt=...` with the image file as the raw body and its `Content-Type` (for example `image/jpeg`); the answer's `data.id` is a `med_...` id. Attach it with `POST https://api-v3.shoprocket.io/v3/stores/{storeId}/products/{productId}/media` and `{"mediaId": "med_..."}`.
4. **Shipping:** a physical product cannot be checked out until the store has a shipping method. Create one with `POST https://api-v3.shoprocket.io/v3/stores/{storeId}/shipping-methods` (example below). Products sent with `"kind": "digital"` need none.
5. **Build the site** with `publishableKey`, following the rest of this page. The script tag and the storefront API need nothing else.
6. **Give the person the claim link** (`claimUrl`) in your final message, and say what it is: "Open this link and sign up to make the store yours. It is in test mode until you connect payments, and it is deleted on (expiresAt) if you do not claim it."

Rules for test stores:

- **The secret key never goes in the site.** Not in browser code, not in a committed file, not in an environment variable the site ships. Use it only for the calls above, then drop it. It stops working when the store is claimed.
- **Until it is claimed, the store is limited:** test checkout only (the built-in test gateway, no card needed), no email is sent (`GET https://api-v3.shoprocket.io/v3/public/{pk}` answers `"unclaimed": true`, and the widget's order confirmation says nothing about an email), at most 25 products and 50 MB of images, and its own hosted shop is hidden from search engines. Every other setting answers `claim_required`; the person sets up payments, their domain and the rest after claiming.
- **One store per site.** Creating stores is limited to 10 per IP address per day. An unclaimed store is deleted 7 days after it was created.

Create a test store:

```bash
curl -X POST "https://api-v3.shoprocket.io/v3/agent-stores" \
  -H "Content-Type: application/json" \
  -d '{"name": "Fern & Clay"}'
```

The answer (example values):

```json
{
  "data": {
    "storeId": "str_...",
    "name": "Fern & Clay",
    "publishableKey": "pk_...",
    "secretKey": "sk_...",
    "claimUrl": "https://app.shoprocket.dev/claim/...",
    "expiresAt": "2026-10-28T09:00:00.000Z",
    "shopUrl": "https://store.shoprocket.dev/fern-clay-1a2b3c",
    "mode": "test",
    "limits": { "products": 25, "mediaBytes": 52428800 }
  }
}
```

Add a product:

```bash
curl -X POST "https://api-v3.shoprocket.io/v3/stores/str_.../products" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{"name": "Speckled Mug", "status": "active",
       "summary": "Hand-thrown stoneware, 350 ml.",
       "variants": [{ "price": 2400, "inventoryQuantity": 50 }]}'
```

A shipping method (flat $5, everywhere):

```bash
curl -X POST "https://api-v3.shoprocket.io/v3/stores/str_.../shipping-methods" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{"name": "Standard shipping",
       "zones": [{ "name": "Everywhere", "isRestOfWorld": true,
                   "rates": [{ "cost": 500 }] }]}'
```

## Rules that keep an integration working

Follow these exactly. Each one is a mistake we have seen.

1. **Load Shoprocket with one script tag, once.** Put `<script src="https://widget.shoprocket.io/loader.js" data-pk="pk_bCz5dnUr-Mdw6bj0BcoE3CALrGA1vhYq"></script>` in the page's `<head>` (in a single-page app: `index.html`). Do not load it twice, do not inject it from a component that re-renders, and do not set `async` or `type="module"` on it.
2. **There is no npm package.** Never `npm install` anything named `shoprocket` or `@shoprocket/...`. The script tag is the whole client.
3. **Use only what this page and the pages it links to name.** Attributes, events, endpoints and fields that are not documented here do not exist. Do not guess names.
4. **Checkout always runs in Shoprocket.** Never build a card form, never collect card details, never call a payment provider yourself. Add to the cart with `Shoprocket.cart.add`, and the shopper checks out from the widget's cart drawer. There is no checkout page or route to build: a "Checkout" or "Cart" button on your site just opens that drawer.
5. **Keep the cart.** Never add `data-no-cart` to the script tag unless you also place a `<div data-shoprocket="cart"></div>` yourself: without a cart element, `Shoprocket.cart.open()` has nothing to open and the shopper cannot check out. To use your own cart button in a header instead of the floating one, give it the attribute `data-shoprocket-cart-toggle` (see "Add to cart and check out").
6. **Money is an integer in the currency's smallest unit.** `2400` with `USD` is $24.00. Format it with `Intl.NumberFormat`, which knows each currency's decimals (JPY has none), never by dividing by 100 and gluing on a "$".
7. **Prices live on variants; the product carries their range.** Each entry in `variants` has its own `id`, `price`, `sellableQuantity` and `optionValues`, and a product with no options still has exactly one variant. For a list, use the product's `priceMin` and `priceMax` ("From" when they differ) instead of looping over variants. Add to the cart by the variant's `id`.
8. **Read the catalogue at runtime.** Fetch it from the API in the browser (or at build time). Never hard-code product names, prices or images: the seller changes them in the dashboard.
9. **Write the publishable key straight into the code.** It is public, like a Stripe publishable key: it is made to be read in the browser and can only read the catalogue and run a shopper's own cart. Put it in the script tag and in one constant (`const PK = 'pk_...'`). Never store it as a project secret, in a secrets manager or in a server-only environment variable: hosted builds (Lovable, Bolt, v0, Vercel, Netlify) leave secrets out of the browser code, and the shop then shows nothing. Never ask the person whether it is safe to expose; it is.
10. **Add to the cart with `Shoprocket.cart.add({ variantId, quantity })`.** It shows the shopper the "Added to cart" card, exactly as the widget's own buttons do. Do not open the cart drawer after an add (the card offers it, and the drawer hides the card), and do not add with `getSdk().cart.addItem`, which changes the cart without telling the shopper.
11. **A product page shows the whole product.** Load it by its slug when the page opens and show the description, every image and the options. A list card links to that page; the product page never links to itself or opens a pop-up of itself. See "A product page".

## Path 1: drop-in embeds

The fastest shop. The widget draws the catalogue, product pages, cart and checkout itself, styled from the dashboard. Put the script tag in `<head>`, then a mount element wherever something should render.

A **floating cart button comes with the script** on every page; you do not add a cart yourself. (`data-no-cart` on the script tag removes the cart entirely, so only use it when you place a `<div data-shoprocket="cart"></div>` of your own.)

| `data-shoprocket` | What renders |
|---|---|
| `catalog` | Product catalog. Browsable product grid or carousel with cart. |
| `product` | Single product. One product's full detail view. |
| `product-view` | Product view. Compact single-product display. |
| `buy-button` | Buy button. A minimal add-to-cart button for one product. |
| `product-card` | Product card. One product as a card: compact, upright or a feature block. |
| `cart` | Cart. The floating cart on its own. |
| `categories` | Categories. Category navigation grid. |
| `account` | Customer account. Shopper login and order history. |
| `search` | Search. A search box or icon that opens live product search (#1177). |

`buy-button`, `product-card`, `product-view` and `product` need `data-product`: the product's slug or its `prod_` id. A buy-button also takes `data-variant` (add that variant directly) and `data-quantity`.

In a single-page app, mounts that appear after the page has loaded (a route change, a component that renders later) are found and mounted automatically. A mount element is mounted once, so when the same element should show a different product, give it a React `key` that changes with the product (`key={slug}`), so React replaces the element.

Want a mount that follows settings the seller saved in the dashboard (layout, colours, which details show)? The **Embeds** page there hands out a snippet with `data-shoprocket="embed" data-embed-id="emb_..."`. More in [Embed the widget](https://v3.shoprocket.io/docs/embedding.md).

A whole shop on one page:

```html
<!-- Once, in <head> -->
<script src="https://widget.shoprocket.io/loader.js" data-pk="pk_bCz5dnUr-Mdw6bj0BcoE3CALrGA1vhYq"></script>

<!-- Anywhere in <body>: the catalogue, with product
     pages, cart and checkout built in -->
<div data-shoprocket="catalog"></div>
```

One product, on a page of your own:

```html
<!-- A card with image, price and add to cart -->
<div data-shoprocket="product-card" data-product="everyday-tote-bag"></div>

<!-- Just the add-to-cart button -->
<div data-shoprocket="buy-button" data-product="everyday-tote-bag"></div>
```

## Path 2: your own design, Shoprocket's checkout

For a shop that must look like the rest of the site: read the catalogue over REST, render it with your own components, and hand the cart and checkout to the widget. Still no server and no secret key.

The storefront API lives at `https://api-v3.shoprocket.io/v3/public/{publishableKey}`. It is plain HTTPS and JSON, open to any origin (CORS `*`), and needs no headers for reads.

| Call | Answers |
|---|---|
| `GET /public/{pk}` | The store: `name`, `currency` (for example `USD`), `logo`. |
| `GET /public/{pk}/products` | A page of products in `data`, with `meta.page`, `meta.totalPages`, `meta.total` and `meta.currency`. Takes `page`, `perPage` (default 24, at most 100), `category` (a category slug), `q` (search) and `sort`. |
| `GET /public/{pk}/products/{slugOrId}` | One product in `data`, by its `slug` or `prod_` id, with the same fields as a list entry. Beside `data`: `related`, an array of products to show alongside it (each with the same fields as a list entry, so a list card component renders them), and `relatedArePairs` (`true` when they are the seller's own "pairs well with" picks, `false` when they are other products from the same category; use it for the heading). A product page fetches this when it opens. |
| `GET /public/{pk}/categories` | The categories in `data`: `id`, `name`, `slug`, `productCount`, `image`. |

Every successful answer wraps its payload in `data`. A product carries more fields than these; the ones below are all a shop needs, and the rest can be ignored. The product fields a shop needs:

- `id` (`prod_...`), `slug`, `name`, `summary` (plain text, may be null), `description` (HTML from the store's editor, may be null).
- `priceMin`, `priceMax`: the cheapest and dearest variant price in integer minor units, for a list card. Both null when the price is on request.
- `categories`: `[{ id, name, slug }]`, in the language asked for with `?locale=`. No second call to `/categories` needed.
- `attributes`: the product's specifications (for example Material), each with `name` and `display` (the value ready to print, for example "250 mm"). Can be empty.
- `images`: an array, each with `url` (the original), `alt`, and `urls.card` (a list thumbnail) and `urls.gallery` (a product-page size). Can be empty.
- `variants`: each with `id` (`var_...`), `price` (integer minor units, or null when the price is on request), `compareAtPrice` (a "was" price, or null), `sellableQuantity` and `inventoryPolicy` (a variant can be bought when `inventoryPolicy` is `continue`, or `sellableQuantity` is null or above 0; otherwise it is sold out), `isDefault` (the variant to preselect) and `optionValues` (`[{ optionName, value }]`, empty on a single-variant product).
- `options`: the choices a shopper makes, `[{ name, values: [{ value }] }]`. Empty when the product has one variant.
- What decides whether your own button can sell it: `personalizationFields` (questions such as an engraving, empty when none), `subscriptionPlans` (repeat-delivery plans, empty when one-off only), `kind` (`physical`, `digital`, `gift_card` or `bundle`) and `purchaseMode` (`buy`, or `enquire` for a quote request). See "A product page".

**Which price to show.** A list card shows `priceMin` (with "From" when `priceMax` is higher); a product page shows the selected variant's `price`, with `compareAtPrice` struck through when it is set. There is no `price` field on the product itself, so never read one. When `priceMin` is null the price is on request: show "Price on request", never $0.00.

Take the currency from the store (`GET /public/{pk}`) or from `meta.currency` on the product list, and never hard-code one: the examples on this page say USD, but the demo store (`pk_bCz5dnUr-Mdw6bj0BcoE3CALrGA1vhYq`, a plant shop called Green Fingers) is priced in GBP, and a real store can use any currency. Option names are the seller's own spelling (`size`, `Pot Colour`); show them as they come.

List products:

```bash
curl "https://api-v3.shoprocket.io/v3/public/pk_bCz5dnUr-Mdw6bj0BcoE3CALrGA1vhYq/products?perPage=12"
```

One product by slug:

```bash
curl "https://api-v3.shoprocket.io/v3/public/pk_bCz5dnUr-Mdw6bj0BcoE3CALrGA1vhYq/products/everyday-tote-bag"
```

The store's name and currency:

```bash
curl "https://api-v3.shoprocket.io/v3/public/pk_bCz5dnUr-Mdw6bj0BcoE3CALrGA1vhYq"
```

A product (trimmed, example values):

```json
{
  "data": {
    "id": "prod_...",
    "slug": "ceramic-pour-over-dripper",
    "name": "Ceramic Pour-Over Dripper",
    "summary": "Hand-thrown, fits most mugs.",
    "description": "<p>...</p>",
    "priceMin": 2400,
    "priceMax": 2600,
    "categories": [{ "id": "cat_...", "name": "Brewing", "slug": "brewing" }],
    "images": [
      { "url": "https://cdn.../dripper.jpg", "alt": "Dripper",
        "urls": { "card": "https://cdn.../r/card/...",
                  "gallery": "https://cdn.../r/gallery/..." } }
    ],
    "options": [
      { "name": "Size", "values": [{ "value": "S" }, { "value": "M" }] }
    ],
    "variants": [
      { "id": "var_...", "price": 2400, "compareAtPrice": null,
        "sellableQuantity": 32, "inventoryPolicy": "deny",
        "isDefault": true,
        "optionValues": [{ "optionName": "Size", "value": "S" }] },
      { "id": "var_...", "price": 2600, "compareAtPrice": null,
        "sellableQuantity": 0, "inventoryPolicy": "deny",
        "isDefault": false,
        "optionValues": [{ "optionName": "Size", "value": "M" }] }
    ]
  }
}
```

## A product page

A list card has room for an image, a name and a price. A product page shows the whole product, so the shopper can choose and buy without a pop-up.

1. **Route on the slug** (`/products/:slug`) and **fetch the product when the page opens**: `GET /public/{pk}/products/{slug}`. Do not build the page from the list card's data, or from data passed along with the link: a shared link or a refresh has no list behind it. Show a "not found" state on a 404.
2. **Show all of it:**
   - every image in `images` as a gallery (`urls.gallery` for the large picture, `urls.card` for thumbnails, `alt` as alt text), and a placeholder when there are none;
   - `name`, and `summary` under it when set;
   - the selected variant's `price` (and `compareAtPrice` struck through when set);
   - one picker per entry in `options` (an option with a single value may be hidden), preselected from the `isDefault` variant, and the variant whose `optionValues` match the choices;
   - `description`, which is HTML from the store's editor: render it as HTML (in React, `dangerouslySetInnerHTML`), not as text;
   - `attributes` as a specification list when there are any, and `related` as cards linking to their own product pages when it is not empty ("Pairs well with" when `relatedArePairs` is true, else "You may also like").
3. **One buy button: "Add to cart"**, which calls `Shoprocket.cart.add({ variantId, quantity })` with the matched variant. Disable it and say "Sold out" when that variant cannot be bought. A refused add already shows the reason in the widget's card, so an inline message of your own is optional. A product page has no "View options" or "View product" button: the options are on the page, and it is the product page.
4. **When your own button cannot sell it, mount the widget's product view instead.** A product with any `personalizationFields`, any `subscriptionPlans`, `kind` `gift_card` or `bundle`, or `purchaseMode` `enquire` needs questions only the widget asks. In place of your own gallery, pickers and button, put `<div data-shoprocket="product" data-product="{slug}"></div>` on the page (in React with `key={slug}`): the full buying screen, inside your own layout. Keep your header, breadcrumbs and related products around it.

**Hosting:** a single-page app with `/products/:slug` routes needs its host to serve `index.html` for every path (a "SPA fallback" or rewrite), or a shared product link answers 404.

**On a list card:** link the card (or a "View" button) to the product page. A card may also carry a quick "Add to cart" (`Shoprocket.cart.add` with its only variant) when the product has exactly one variant, can be bought, and needs nothing from point 4; otherwise its button goes to the product page. Do not open the widget's pop-up (`Shoprocket.product.openModal`) from a site that has its own product pages.

src/pages/Product.jsx (React):

```jsx
import { useEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { API, addToCart, canBuy, formatPrice, getStore } from '../shoprocket';

// Products that need the widget's own buying screen (point 4).
const needsWidget = (p) =>
  p.personalizationFields.length > 0 || p.subscriptionPlans.length > 0 ||
  p.kind === 'gift_card' || p.kind === 'bundle' || p.purchaseMode === 'enquire';

export default function Product() {
  const { slug } = useParams();
  const [currency, setCurrency] = useState(null);
  const [product, setProduct] = useState(null);
  const [related, setRelated] = useState([]);
  const [choice, setChoice] = useState({});
  const [image, setImage] = useState(0);
  const [error, setError] = useState(null);

  useEffect(() => { getStore().then((s) => setCurrency(s.currency)); }, []);

  useEffect(() => {
    setProduct(null);
    fetch(`${API}/products/${encodeURIComponent(slug)}`)
      .then((r) => (r.ok ? r.json() : Promise.reject(r.status)))
      .then(({ data, related }) => {
        setProduct(data);
        setRelated(related);
        setImage(0);
        const def = data.variants.find((v) => v.isDefault) ?? data.variants[0];
        setChoice(Object.fromEntries(def.optionValues.map((o) => [o.optionName, o.value])));
      })
      .catch(() => setError('not-found'));
  }, [slug]);

  const variant = useMemo(() => product?.variants.find((v) =>
    v.optionValues.every((o) => choice[o.optionName] === o.value)), [product, choice]);

  if (error) return <p>Product not found.</p>;
  if (!product || !currency) return <p>Loading...</p>;

  const pic = product.images[image];
  return (
    <article>
      <h1>{product.name}</h1>
      {needsWidget(product) ? (
        <div key={product.slug} data-shoprocket="product" data-product={product.slug} />
      ) : (
        <>
          {pic ? <img src={pic.urls.gallery} alt={pic.alt ?? product.name} /> : <div className="placeholder" />}
          {product.images.map((img, i) => (
            <button key={img.url} onClick={() => setImage(i)}><img src={img.urls.card} alt="" /></button>
          ))}
          {product.summary && <p>{product.summary}</p>}
          <p>{variant && variant.price != null ? formatPrice(variant.price, currency) : 'Price on request'}</p>
          {product.options.map((opt) => (
            <label key={opt.name}>{opt.name}
              <select value={choice[opt.name] ?? ''}
                onChange={(e) => setChoice({ ...choice, [opt.name]: e.target.value })}>
                {opt.values.map((v) => <option key={v.value}>{v.value}</option>)}
              </select>
            </label>
          ))}
          <button disabled={!variant || !canBuy(variant)} onClick={() => addToCart(variant.id)}>
            {variant && !canBuy(variant) ? 'Sold out' : 'Add to cart'}
          </button>
          {product.description && <div dangerouslySetInnerHTML={{ __html: product.description }} />}
        </>
      )}
      {/* related: render as cards linking to /products/{slug} */}
    </article>
  );
}
```

## Add to cart and check out

The script tag from the rules is still required on this path: it brings the cart drawer and checkout. Your own buttons talk to it through `window.Shoprocket`.

1. **Wait for the widget.** `window.Shoprocket` exists only after the script has loaded, which is usually after your code runs. When it is ready it sets `window.__shoprocketReady = true` and fires a `shoprocket:ready` event on `window`. Check the flag, else wait for the event. Calling `Shoprocket` before then throws.
2. **Add a line:** `await Shoprocket.cart.add({ variantId, quantity })`. The shopper sees the "Added to cart" card with View cart and Checkout buttons, as with the widget's own buttons. It resolves with the new cart, and rejects with an `Error` whose `message` says why when the store refuses (sold out, for example; the widget shows that reason too). Use the variant the shopper picked; for a single-variant product that is its only variant. **Do not open the drawer after adding**: it would hide the card.
3. **Checkout:** the card's Checkout button, the floating cart button or your own cart button opens the drawer, where the shopper reviews the cart and pays. `Shoprocket.cart.open()` opens it from a button of yours ("Cart", "Checkout"). Payment happens in the store's own gateway, so there is nothing more to build: no checkout page, no payment code, no thank-you page.
4. **Your own cart button:** add `data-shoprocket-cart-toggle` to it (any element: a `<button>` or an `<a href="/cart">`). The widget then opens the drawer when it is clicked (also for a button your framework renders after the page loaded), writes the item count to its `data-count` attribute, and hides the floating cart button while yours is on screen. To draw the count as text yourself, listen for `shoprocket:cart:updated` on `window`; `event.detail.cart.itemCount` is the total quantity. It also fires when a returning shopper's cart loads, so a count that starts at 0 catches up on its own.

There is no checkout page to build and no order confirmation page: checkout, payment and the "order placed" screen all happen in the drawer.

Some products need choices only the widget asks for (a personalised engraving, a subscription plan, a gift card's recipient, a bundle's parts, a quote request). On your product page, mount the widget's product view for those, as in "A product page". On a site with no product pages of its own, `Shoprocket.product.openModal(slug)` opens the same screen in a pop-up.

Every method and event is listed in the [widget JavaScript API](https://v3.shoprocket.io/docs/javascript.md).

Add to cart from your own button:

```js
// Resolves with window.Shoprocket once the widget has started.
function shoprocketReady() {
  return new Promise((resolve) => {
    if (window.__shoprocketReady) return resolve(window.Shoprocket);
    window.addEventListener('shoprocket:ready',
      () => resolve(window.Shoprocket), { once: true });
  });
}

// Shows the "Added to cart" card. Throws with a readable message
// when the store refuses (sold out, for example).
async function addToCart(variantId, quantity = 1) {
  const sr = await shoprocketReady();
  return sr.cart.add({ variantId, quantity });
}

// A cart count of your own (or put data-shoprocket-cart-toggle on
// your cart button and style its data-count attribute)
window.addEventListener('shoprocket:cart:updated', (e) => {
  document.querySelector('#cart-count').textContent =
    e.detail.cart ? e.detail.cart.itemCount : 0;
});
```

Format a price:

```js
function formatPrice(minor, currency) {
  const digits = new Intl.NumberFormat('en-US', { style: 'currency', currency })
    .resolvedOptions().maximumFractionDigits ?? 2;
  return new Intl.NumberFormat('en-US', { style: 'currency', currency })
    .format(minor / 10 ** digits);
}

formatPrice(2400, 'USD'); // "$24.00"
formatPrice(2400, 'JPY'); // "¥2,400"
```

## React (Vite, Lovable, Bolt, v0)

The same two pieces in a React app: the script tag in `index.html`, and a small hook around `window.Shoprocket`. Nothing to install.

- Put the script tag in `index.html`, not in a component, with the publishable key written into it. Write the same key into one constant in `src/shoprocket.ts`. Not an environment variable or project secret: it is public, and a hosted build without the variable shows an empty shop.
- Fetch products with `fetch` in an effect (or your data library). The answers are cacheable and need no auth header.
- Product pages: route on the product's `slug` (`/products/:slug`) and load `GET /products/{slug}` when the page opens; "A product page" above has a complete one.
- With options (size, colour), find the variant whose `optionValues` match the shopper's choices, and disable "Add to cart" while none matches or the match cannot be bought (the rule in Path 2: `inventoryPolicy` is `continue`, or `sellableQuantity` is null or above 0). Compare `optionValues[].optionName` with `options[].name` exactly as the API spells them. Preselect the `isDefault` variant's values so the page opens on a buyable choice. A picker for an option with a single value adds nothing; it is fine to hide it.
- In TypeScript, declare what you use on `window` (as in the example) rather than casting to `any` everywhere.

index.html:

```html
<head>
  <!-- ... -->
  <!-- The publishable key is public: write it in, never a secret -->
  <script src="https://widget.shoprocket.io/loader.js" data-pk="pk_bCz5dnUr-Mdw6bj0BcoE3CALrGA1vhYq"></script>
</head>
```

src/shoprocket.ts:

```js
// Public by design, like a Stripe publishable key: keep it in the code.
export const PK = 'pk_bCz5dnUr-Mdw6bj0BcoE3CALrGA1vhYq';
export const API = `https://api-v3.shoprocket.io/v3/public/${PK}`;

declare global {
  interface Window {
    __shoprocketReady?: boolean;
    Shoprocket?: {
      cart: {
        add(p: { variantId: string; quantity?: number }): Promise<unknown>;
        open(): void;
      };
    };
  }
}

export function shoprocketReady(): Promise<NonNullable<Window['Shoprocket']>> {
  return new Promise((resolve) => {
    if (window.__shoprocketReady && window.Shoprocket) return resolve(window.Shoprocket);
    window.addEventListener('shoprocket:ready', () => resolve(window.Shoprocket!), { once: true });
  });
}

// The store's name and currency, fetched once.
let store: Promise<{ name: string; currency: string }> | undefined;
export const getStore = () =>
  (store ??= fetch(API).then((r) => r.json()).then((j) => j.data));

export async function getProducts(page = 1) {
  const res = await fetch(`${API}/products?perPage=24&page=${page}`);
  if (!res.ok) throw new Error(`Shoprocket ${res.status}`);
  return res.json(); // { data: Product[], meta: { totalPages, currency, ... } }
}

// Shows the "Added to cart" card; throws with a readable message when refused.
export async function addToCart(variantId: string, quantity = 1) {
  return (await shoprocketReady()).cart.add({ variantId, quantity });
}

// A variant can be bought when it oversells or has stock left.
export const canBuy = (v: { inventoryPolicy: string; sellableQuantity: number | null }) =>
  v.inventoryPolicy === 'continue' || v.sellableQuantity === null || v.sellableQuantity > 0;

// A list card's price: "From" when the variants differ, never $0.00.
export function listPrice(p: { priceMin: number | null; priceMax: number | null }, currency: string) {
  if (p.priceMin === null) return 'Price on request';
  const from = p.priceMax !== null && p.priceMax > p.priceMin ? 'From ' : '';
  return from + formatPrice(p.priceMin, currency);
}

export function formatPrice(minor: number, currency: string) {
  const fmt = new Intl.NumberFormat('en-US', { style: 'currency', currency });
  return fmt.format(minor / 10 ** (fmt.resolvedOptions().maximumFractionDigits ?? 2));
}
```

A cart count hook:

```jsx
import { useEffect, useState } from 'react';

export function useCartCount() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const on = (e) => setCount(e.detail.cart ? e.detail.cart.itemCount : 0);
    window.addEventListener('shoprocket:cart:updated', on);
    return () => window.removeEventListener('shoprocket:cart:updated', on);
  }, []);
  return count;
}
```

## Test a checkout

A new store starts in [test mode](https://v3.shoprocket.io/docs/test-mode.md). Until the seller connects a real way to pay, its checkout offers a built-in **test gateway**: press approve or decline, no card needed, and the order is marked as a test. A store whose gateway is in its own sandbox (Stripe test mode) takes that gateway's test cards, such as `4242 4242 4242 4242` with any future expiry and any CVC.

To check an integration end to end: add a product, open the cart, check out with a US address (for example 1 Main St, Portland, OR 97201), pay with the test method, and see the order confirmation in the drawer. The order then appears in the seller's dashboard under **Orders**.

## Common mistakes

- **A price shows as 2400.00 or $2,400.** The amount is in minor units: format it as above.
- **"Shoprocket is not defined" or "Cannot read properties of undefined".** Code called `window.Shoprocket` before the widget loaded. Wait for it as in the example.
- **The cart drawer never opens.** The script tag is missing, carries `data-no-cart` with no cart mount on the page, or was added from a component and loaded twice. It belongs once in `<head>`, without `data-no-cart`.
- **Add to cart answers 422 or 409.** Send a `variantId` from the product's `variants`, not the product id or an option value; and check the variant is not sold out.
- **Every product shows the same price, or $0.00.** Each variant has its own; show the selected variant's `price` on a product page, and `priceMin` with "From" on a list. The product has no `price` field.
- **"The store publishable key has not been configured" on the live site.** The key was stored as a secret or an environment variable the hosted build does not include. It is public: write it into the code.
- **The cart drawer opens on every add, and the "Added to cart" card never shows.** The code adds with `getSdk().cart.addItem` and then calls `cart.open()`. Use `Shoprocket.cart.add` and nothing else.
- **The product page shows only an image, a summary and a price.** It was built from the list card. Fetch `GET /products/{slug}` on the page and show the description, gallery and options.
- **A product's image is missing.** `images` can be empty; show a placeholder.
- **An embed stays empty.** Its `data-product` names a slug that does not exist in this store, or the key belongs to a different store.

## Reference

- [Embed the widget](https://v3.shoprocket.io/docs/embedding.md): every embed type and attribute.
- [Widget JavaScript API](https://v3.shoprocket.io/docs/javascript.md): every `window.Shoprocket` method and every `shoprocket:*` event.
- [API basics](https://v3.shoprocket.io/docs/basics.md): conventions, errors, pagination, and a fully headless checkout with no widget at all.
- [REST API reference](https://v3.shoprocket.io/docs/api.md): every endpoint, generated from the API. The machine-readable OpenAPI document is `https://api-v3.shoprocket.io/openapi.json`.
- [Test mode](https://v3.shoprocket.io/docs/test-mode.md): the test gateway and test orders.

---

- HTML version: https://v3.shoprocket.io/docs/quickstart
- Language: English (en)
