# Widget JavaScript API

> Drive the Shoprocket widget from your own scripts and listen to what shoppers do: open the cart, add to it, and send purchases to GA4, Meta or any analytics tool.

Drive the widget from your own scripts, and hear what shoppers do in it: wire custom buttons, analytics and conversion pixels.

## Events and window.Shoprocket

The widget gives your page two things:

- **Events.** Browser `CustomEvent` s dispatched on `window`, all named `shoprocket:...`, carrying what happened in `event.detail`. You can add a listener at any time, even before the widget's script tag, because the listener simply waits.
- **`window.Shoprocket`.** An object with methods to open the cart, open a product, read the cart and more. It exists once the widget's script has run, so code that calls it has to wait for it (next section).

Everything on this page works on any page that carries the [embed snippet](https://v3.shoprocket.io/docs/embedding.md), and on a store's hosted shop.

Listen, then act

```
<script src="https://widget.shoprocket.io/loader.js" data-pk="pk_bCz5dnUr-Mdw6bj0BcoE3CALrGA1vhYq" async></script>
<script>
  var bag = document.querySelector('#bag');

  // Works before or after the widget has loaded.
  addEventListener('shoprocket:cart:updated', function (e) {
    bag.textContent = e.detail.cart.itemCount;
  });

  // window.Shoprocket exists once it has loaded.
  bag.addEventListener('click', function () {
    if (window.Shoprocket) Shoprocket.cart.open();
  });
</script>
```

## Before the widget loads

The loader script is asynchronous, so your own scripts usually run first. There is no call queue: calling `Shoprocket.cart.open()` before the widget has loaded throws, because `window.Shoprocket` is not there yet.

When the widget has finished starting up (store loaded, cart listening, embeds mounted), it dispatches `shoprocket:ready` on `window` and sets `window.__shoprocketReady = true`. Check the flag first and fall back to the event, as in the example, so code that starts late does not wait for an event that has already fired. The cart may still be on its way at that moment: `Shoprocket.cart.data` is `null` until `shoprocket:cart:loaded`.

Once `window.Shoprocket` exists, `Shoprocket.whenReady()` returns a promise for the same moment. It rejects if the widget could not start (a wrong key, the API unreachable); `shoprocket:ready` simply never fires in that case.

Run code once it is ready

```
function withShoprocket(fn) {
  if (window.__shoprocketReady) return fn(Shoprocket);
  addEventListener('shoprocket:ready', function () {
    fn(Shoprocket);
  }, { once: true });
}

withShoprocket(function (sr) {
  if (location.hash === '#basket') sr.cart.open();
});
```

Or with a promise

```
// Only once window.Shoprocket exists.
await Shoprocket.whenReady();
Shoprocket.product.openModal('my-product');
```

## Controlling the widget

| Method | What it does |
| --- | --- |
| `Shoprocket.cart.open()` | Open the cart drawer. Also close() and toggle(). |
| `Shoprocket.cart.add({ variantId, quantity })` | Add to the cart from your own button, with the "Added to cart" card. A promise. See [below](#add-to-cart). |
| `Shoprocket.cart.data` | The cart as the widget holds it (the same object as `Shoprocket.cart.get()`), or `null` before it has loaded. Shape: see cart events below. |
| `Shoprocket.product.openModal(slugOrId)` | Open a product in a pop-up, by slug or prod\_ id. Works on any page, with or without a catalogue embed. |
| `Shoprocket.product.open(id?)` | Open a product inside the catalogue embed on the page, by `prod_` id (the first product when omitted). Also `openBySlug(slug)` and `close()`, which goes back to the list. Needs a catalogue embed. |
| `Shoprocket.account.open(tab?)` | Open the shopper's account drawer, on `'orders'`, `'subscriptions'` or `'details'`. Also `close()`. |
| `Shoprocket.search.open(query?)` | Open the search panel, optionally with a query typed in. Also `close()`. |
| `Shoprocket.store.data` | The store's public settings the widget loaded (also `store.get()`). |
| `Shoprocket.rescan()` | Mount any `data-shoprocket` element added to the page since it loaded. Call it after your own code (or a single-page app's router) renders new mounts. |
| `Shoprocket.setLanguage(code)` | Switch the widget's language in place, for example 'fr'. Only a language the store has enabled; anything else falls back as the store would. |
| `Shoprocket.whenReady()` | A promise that resolves once the widget has started. See above. |
| `Shoprocket.getSdk()` | The widget's own storefront API client, signed in as the same shopper with the same cart. Throws if called before the widget has started. |

`Shoprocket.auth` (`isLoggedIn()`, `logout()`, and the email code sign-in) is there for custom account screens; most pages never need it.

## Your own add-to-cart button

The simplest add-to-cart button is the widget's own: a [buy-button mount](https://v3.shoprocket.io/docs/embedding.md#inline) with `data-product`, styled from the dashboard. It handles variants, stock and the "added" notice, and fires every event below.

For a button you draw yourself, call `Shoprocket.cart.add({ variantId, quantity })`. It adds the way the widget's own buttons do: the shopper sees the "Added to cart" card (pointing at your own `data-shoprocket-cart-toggle` button when the page has one, else at the floating cart), with View cart and Checkout buttons. `shoprocket:product:added`, `shoprocket:cart:updated` and the `shoprocket:add_to_cart` analytics event fire, so the store's pixels hear about it.

- Pass `variantId` (a `var_` id from the product's `variants`), or `productId` alone for a product's first variant. `quantity` defaults to 1.
- It returns a promise that resolves with the new cart. When the store refuses the add (sold out, not enough stock, a product that needs a choice only the widget can ask for) it rejects with an `Error` whose `message` says why, and the widget shows the same reason in its error card.
- Do not open the cart drawer after an add: the card already offers it, and opening the drawer hides the card. Call `Shoprocket.cart.open()` only from a cart or checkout button.
- It is safe to call straight after `shoprocket:ready`, or before it: an add made while the cart is still loading waits for it. On a page with no cart at all (`data-no-cart` on the script tag and no cart mount) it rejects at once.

getSdk() adds silently

`Shoprocket.getSdk().cart.addItem()` also adds to the shopper's cart, and the drawer shows the line, but it shows no card and fires no `shoprocket:product:added` or analytics event. Use it for scripted changes the shopper did not click for; use `Shoprocket.cart.add` for a shopper's own button.

Never create a second SDK client with its own cart for this: the widget would not see its lines, and the shopper would end up with two baskets.

A button of your own

```
<button id="buy">Add to cart</button>
<p id="buy-error" hidden></p>
<script>
  var buy = document.querySelector('#buy');
  buy.addEventListener('click', async function () {
    if (!window.Shoprocket) return; // not loaded
    await Shoprocket.whenReady();
    try {
      // Shows the "Added to cart" card, as the widget's own buttons do.
      await Shoprocket.cart.add({
        variantId: 'var_034oxcMjuCaVaXzlIaEcfs',
        quantity: 1,
      });
    } catch (err) {
      // Refused (sold out, for example). The widget shows its error
      // card too; err.message says why.
      document.querySelector('#buy-error').textContent = err.message;
      document.querySelector('#buy-error').hidden = false;
    }
  });
</script>
```

## Your own sign-in link

Shoppers can sign in from the floating cart button: the person icon after the cart opens the account drawer, and shows their initials once they are signed in. You can turn it off in Embeds, Floating cart, Sign-in.

To put sign-in in your own header instead, add `data-shoprocket-account-toggle` to any element. A click opens the account drawer rather than following the link, and the element carries `data-signed-in` while a shopper is signed in, so you can restyle it. A link to `#!/login` or `#!/account` works too, and so does `Shoprocket.account.open()` from your own script. All three work on pages with no cart.

An Account link in your header

```
<!-- Any element: the widget opens the account drawer on click -->
<a href="/account" data-shoprocket-account-toggle>Account</a>

<!-- Or a plain link, no attribute needed -->
<a href="#!/login">Sign in</a>

<style>
  /* Present while a shopper is signed in on this browser */
  [data-shoprocket-account-toggle][data-signed-in] { font-weight: 600; }
</style>
```

## Cart events

| Event | When | event.detail |
| --- | --- | --- |
| `shoprocket:ready` | The widget has started. | none |
| `shoprocket:cart:loaded` | The cart has been fetched: on page load, and again after an order, when a fresh cart starts. | `{ cart }` |
| `shoprocket:cart:updated` | The cart changed, whatever changed it: its first load, an add (once when it appears, again when the server confirms), a quantity, a removal, a discount code, a shipping choice, or your own script through `getSdk()`. | `{ cart }` |
| `shoprocket:product:added` | A shopper added something with one of the widget's buttons. Fires as soon as the line appears, before the server confirms it; if the server then refuses, `shoprocket:cart:error` follows. | `{ product: { id, name, price, media, variantText?, quantity }, checkout? }`. `variantText` is the variant's name, absent for a product with one variant. `checkout: true` when it was a Buy now. |
| `shoprocket:cart:error` | A cart change failed. | `{ message, type?, availableQuantity?, currentQuantity? }`. `message` is shopper-facing and translated. `type` is `out_of_stock`, `insufficient_stock` (with the two quantities) or `invalid`, and absent for other failures. |

The `cart` in these events is the storefront API's cart. The fields most integrations read:

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | The cart's cart\_ id. An empty string until the first line has been added and the cart exists on the server. |
| `itemCount` | integer | Total quantity across every line. |
| `currencyCode` | string | The store's currency, for example USD. |
| `totals.total` | integer | What the shopper pays, in minor units. While an add is still on its way it can lag the lines by a moment; the confirmed cart:updated that follows has it right. |
| `items[]` | array | The lines: id, productId, variantId, name, variantName, unitPrice (minor units), quantity, subtotal. |

Keep a badge in step

```
addEventListener('shoprocket:cart:updated', function (e) {
  var cart = e.detail.cart;
  badge.textContent = cart ? cart.itemCount : 0;
});
```

shoprocket:product:added detail

```
{
  "product": {
    "id": "prod_033xI1QNJIX9ij7joWsltl",
    "name": "Aloe vera with white pot",
    "price": 799,
    "media": { "url": "https://cdn...", ... },
    "quantity": 1
  }
}
```

## Analytics events

Alongside the cart events, the widget reports each step of the shopping journey as `shoprocket:<name>`, with names and fields modelled on Google Analytics 4's ecommerce events. These are the events the store's own analytics and its ad pixels are fed from, trimmed to what a tracker needs: no names, emails or addresses.

| Event | When | event.detail |
| --- | --- | --- |
| `shoprocket:view_item` | A product page or pop-up opened. | An item |
| `shoprocket:view_item_list` | A catalogue page was shown. | `{ item_list_name, currency, item_count, items }`, the first five products |
| `shoprocket:select_item` | A product was clicked in the catalogue. | An item |
| `shoprocket:add_to_cart` | A shopper added a product with the widget. | An item, with quantity |
| `shoprocket:remove_from_cart` | A line was removed. | An item, with quantity |
| `shoprocket:cart_quantity_updated` | A line's quantity changed. | An item, plus `old_quantity`, `new_quantity`, `delta` |
| `shoprocket:cart_opened` | The cart drawer opened. Also cart_closed. | A cart summary |
| `shoprocket:begin_checkout` | The shopper started checkout. | A cart summary |
| `shoprocket:purchase` | An order was placed (see below). | An order |

| Field | Type | Description |
| --- | --- | --- |
| `An item` | object | item_id (the prod\_ id), variant_id (the var\_ id, when the exact variant is known: always on a cart line, and on a product with only one), item_name, price_cents (one unit), currency, quantity, variant (the variant's name, or its var\_ id when it has none). view_item and select_item also have item_slug, and category or brand when known. |
| `A cart summary` | object | cart_id, currency, value_cents (the cart total), item_count. |
| `An order` | object | order_id (the ord\_ id), transaction_id (the order number the shopper sees), value_cents (the order total), tax_cents, shipping_cents, currency, items (each an item). |

shoprocket:add_to_cart detail

```
{
  "item_id": "prod_033xI1QNJIX9ij7joWsltl",
  "item_name": "Aloe vera with white pot",
  "price_cents": 799,
  "currency": "USD",
  "quantity": 1,
  "variant": "var_033xI1QVz3m8lCzM9BeoTy",
  "variant_id": "var_033xI1QVz3m8lCzM9BeoTy"
}
```

shoprocket:purchase detail

```
{
  "order_id": "ord_...",
  "transaction_id": "1042",
  "value_cents": 3100,
  "tax_cents": 300,
  "shipping_cents": 600,
  "currency": "EUR",
  "items": [
    { "item_id": "prod_...", "item_name": "House blend beans",
      "price_cents": 1100, "currency": "EUR",
      "quantity": 2, "variant": "1kg",
      "variant_id": "var_..." }
  ]
}
```

`shoprocket:purchase` fires once per order, in the tab where checkout finishes: when a card payment succeeds, when the shopper comes back paid from a payment page (PayPal, a bank redirect), and when an order is placed with an offline method such as bank transfer, which is unpaid at that moment. A shopper who pays on a payment page and never comes back fires nothing in the browser, so for revenue you must not miss, use the [`order.paid` webhook](https://v3.shoprocket.io/docs/webhooks.md) on your server.

Checkout also reports its steps for funnel reports: `checkout_contact_information_viewed` and `_completed`, `checkout_shipping_address_viewed` and `_completed`, `checkout_payment_method_viewed` and `_completed`, `checkout_payment_selected`, `checkout_payment_redirect`, `checkout_error` and `checkout_abandoned`, each prefixed `shoprocket:`. Use these by name only: their detail carries nothing to rely on.

## Money in events

Every amount in every event is an integer in the currency's smallest unit, as it is everywhere in the API: `799` with `USD` is $7.99. In the analytics events the field names end in `_cents`; in the cart events they are the cart's own fields (`unitPrice`, `totals.total`) and `product.price`.

GA4, Meta and most trackers want a decimal, so convert, and use the currency's own number of decimals: `JPY` has none and `KWD` has three, so dividing by 100 would report a Japanese store's sales a hundred times too low. The `toMajor` function in the example uses the same table the widget does.

Amounts are in the store's currency (`currency` in the event), even when the shopper chose to see prices in another.

Convert for a tracker

```
// Integer minor units -> a decimal amount. Not every currency has
// two decimals, so never divide by 100 blindly.
const ZERO_DECIMAL = [
  'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA',
  'PYG', 'RWF', 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF'
];
const THREE_DECIMAL = [
  'BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND'
];

function toMajor(minor, currency) {
  const code = String(currency).toUpperCase();
  const exponent = ZERO_DECIMAL.includes(code) ? 0 : THREE_DECIMAL.includes(code) ? 3 : 2;
  return minor / Math.pow(10, exponent);
}
```

## Conversion tracking

Using GA4, Meta or TikTok? There may be nothing to write

A seller who enters their Google Analytics 4 measurement ID, Meta pixel ID or TikTok pixel ID in the dashboard (**Settings > Tracking**) gets these events sent for them, with consent handled. Do not also send them yourself to the same ID, or every sale counts twice.

For any other tool, or a pixel you manage yourself, listen for the analytics events and forward them. The examples assume your tag (gtag or fbq) is already on the page, and the `toMajor` function from the section above.

A `shoprocket:purchase` fires once per order, but if your page can run the same handler twice, key on `order_id`. It is also a good event id for platforms that match browser events with server ones, as Meta does with `eventID`.

For catalogue ads, send `variant_id` where there is one. The product feed lists one item per variant, keyed by its `var_` id and grouped by the `prod_` id, so a variant id matches an item exactly. A product id only matches the group: on Meta, send it with `content_type: 'product_group'`.

Google Analytics 4

```
window.addEventListener('shoprocket:purchase', function (e) {
  var d = e.detail;
  gtag('event', 'purchase', {
    transaction_id: d.transaction_id,
    value: toMajor(d.value_cents, d.currency),
    tax: toMajor(d.tax_cents, d.currency),
    shipping: toMajor(d.shipping_cents, d.currency),
    currency: d.currency,
    items: d.items.map(function (i) {
      return {
        item_id: i.item_id,
        item_name: i.item_name,
        item_variant: i.variant,
        price: toMajor(i.price_cents, d.currency),
        quantity: i.quantity,
      };
    }),
  });
});
```

Meta Pixel

```
window.addEventListener('shoprocket:purchase', function (e) {
  var d = e.detail;
  fbq('track', 'Purchase', {
    value: toMajor(d.value_cents, d.currency),
    currency: d.currency,
    content_type: 'product',
    content_ids: d.items.map(function (i) { return i.variant_id || i.item_id; }),
    num_items: d.items.reduce(function (n, i) { return n + i.quantity; }, 0),
  }, { eventID: d.order_id });
});

window.addEventListener('shoprocket:add_to_cart', function (e) {
  var d = e.detail;
  fbq('track', 'AddToCart', {
    value: toMajor(d.price_cents * d.quantity, d.currency),
    currency: d.currency,
    content_type: d.variant_id ? 'product' : 'product_group',
    content_ids: [d.variant_id || d.item_id],
  });
});
```

## The placed order

Next to `shoprocket:purchase`, `shoprocket:order:placed` carries the order's full receipt: the same object as the storefront API's `GET /orders/{orderId}`, with `orderNumber`, `email`, `customerName`, `items`, every total and the addresses. It is for your own page, for example to show something after checkout.

It holds personal data

Do not forward `order:placed` to an analytics or advertising tool. Use `shoprocket:purchase` for that, which has no names, emails or addresses in it.

Your own thank-you

```
window.addEventListener('shoprocket:order:placed', function (e) {
  var order = e.detail;
  console.log('Order', order.orderNumber, 'for', order.email);
});
```

## Consent

The pixels a seller sets up in **Settings > Tracking** respect the shopper's consent. The widget reads it from Google Consent Mode (`ad_storage`) if your page already uses it, and treats a browser's Global Privacy Control signal as a no. If your banner uses neither, tell the widget directly with a `shoprocket:consent` event: `marketing` governs the ad pixels, and `analytics` is read when `marketing` is not given.

This only governs the pixels Shoprocket loads. Events still fire either way: what your own listeners do with them is under your own consent setup.

From your cookie banner

```
// When the shopper accepts marketing cookies:
window.dispatchEvent(new CustomEvent('shoprocket:consent', {
  detail: { marketing: true },
}));

// And when they refuse:
window.dispatchEvent(new CustomEvent('shoprocket:consent', {
  detail: { marketing: false },
}));
```

## Older v2 embeds

A page still carrying the Shoprocket v2 snippet (loaded from `cdn.shoprocket.io/loader.js`) also gets v2's own `window.Shoprocket` calls, such as `cart.add()` and `events.listen()`, so existing scripts keep working. Those exist only on such pages, not with the new snippet. New code should use what is on this page, which works on both. [Moving from v2](https://v3.shoprocket.io/docs/moving-from-v2.md#page-scripts) maps each v2 call to its replacement.

---

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