Skip to content

JavaScript API

window.OpenShop is the supported way for your own site code to talk to Open Shop. It reads customer, cart, store, product, and referral state, performs cart actions, and emits events when anything changes.

Nothing extra to load. The same components.js that registers the os-* custom elements installs window.OpenShop as it boots:

index.html
<link rel="stylesheet" href="https://openshopgo.com/wc/components.css" />
<script src="https://openshopgo.com/wc/components.js" defer></script>

If the Quickstart tags are already on your page, the API is already there. OpenShop.version reports the bundle version that installed it.

components.js carries defer, so it runs after the HTML is parsed. Any inline <script> earlier in the page runs before window.OpenShop exists. Pick the path that matches where your code lives.

Push a callback onto window.OpenShopQ. This is the same pattern as a dataLayer queue: the array exists whether the bundle has loaded or not.

Site header custom code
<script>
(window.OpenShopQ ??= []).push((os) => {
console.log('Open Shop', os.version, 'is ready');
});
</script>

On init the bundle drains the queue, then replaces push with an immediate call — so callbacks pushed later still run right away. This snippet is safe anywhere on the page, which makes it the one to reach for in a site builder where you do not control script order.

Use the ready promise.

app.js
OpenShop.ready.then((os) => {
console.log('Open Shop', os.version, 'is ready');
});

The bundle also dispatches os:ready on document, for code that only wants addEventListener.

app.js
document.addEventListener('os:ready', (event) => {
const os = event.detail;
console.log('Open Shop', os.version, 'is ready');
});

Every current property is a snapshot: a frozen plain object built from the bundle’s internal state. On each change the snapshot is replaced wholesale rather than mutated, which means:

  • === is a valid change check. If the reference is the same, nothing changed.
  • Nothing you do to a snapshot affects the store. The objects are frozen.
  • Promise results are the same snapshots, so await OpenShop.customer.get() and OpenShop.customer.current hand you the same object once resolution finishes.

Two empty-looking values mean different things, and the difference matters:

ValueMeaning
undefinedNot resolved yet. The request is in flight, or has not started. Do not draw a UI from this.
nullResolved, and there is nothing. No signed-in customer, no cart, no referral.

Only customer.current and referral.current have the unresolved state. cart.current is CartSummary | null and is never undefined — for the cart, null simply means there is nothing to show.

app.js
const customer = OpenShop.customer.current;
if (customer === undefined) {
// Still resolving — wait for customer:change instead of painting anything.
} else if (customer === null) {
showLoginLink();
} else {
showGreeting(customer.firstName);
}

Subscribe with OpenShop.on(event, handler). It returns an unsubscribe function.

app.js
const off = OpenShop.on('cart:change', (summary) => {
console.log('cart now has', summary?.itemCount ?? 0, 'items');
});
// Later
off();
EventdetailFires when
readyOpenShopInit is complete.
customer:changeCustomer | nullFirst resolution, a detected login or logout, or get({ refresh: true }).
cart:changeCartSummary | nullAny summary replacement, including refreshes triggered by another tab.
cart:openundefinedThe slideout opens.
cart:closeundefinedThe slideout closes.
cart:add{ item, summary }An add to cart succeeds. item is null when the added line is not in the refreshed summary.
cart:errorOpenShopErrorA mutation through OpenShop.cart.add/update/remove fails. Clicks on the components’ own buttons do not fire it.
referral:changeReferral | nullThe tracker resolves or clears attribution.

Every event is also dispatched on document as a CustomEvent named os: plus the event name — os:ready, os:customer:change, os:cart:open, and so on — carrying the same detail. Use this when you cannot hold a reference to the namespace, such as a one-off embed or a tag manager snippet.

Site footer custom code
document.addEventListener('os:customer:change', (event) => {
const customer = event.detail;
document.body.classList.toggle('is-signed-in', customer !== null);
});

Both paths deliver the same payloads. on() is easier to unsubscribe from; the document events are easier to attach from anywhere.

Rejected promises reject with an OpenShopError:

openshop.d.ts
interface OpenShopError {
code: string;
status: number;
message: string;
}

code is stable enough to branch on. status is the HTTP status. message is a short description — useful for logs, not written for shoppers.

These are the backend’s own codes, passed through untouched. You get them from cart.add() and from api.get / api.post.

codeWhat it means
UNPROCESSABLE_ENTITYThe product is out of stock.
GONEThe product is no longer available.
FORBIDDENOn an add, the product is not available to this shopper’s account type. On a quantity update, the line is externally priced and its quantity cannot change.
DUPLICATEThe cart already holds an enrollment, and enrollments cannot be added twice.
CONFLICTSeveral causes — an inactive cart, or a competing enrollment item. Read message for the specific one.
INVALID_TOKENNot signed in, or the session has ended.

When a response carries no code of its own, the status is used to pick one: BAD_REQUEST, NOT_FOUND, or RATE_LIMITED.

These are raised before or around the request, so status may be 0.

codeWhat it means
INVALID_INPUTcart.add() was called with neither productId nor slug.
INVALID_PATHapi.get or api.post was given a path that does not start with a single /. Absolute URLs and protocol-relative // paths are refused.
INVALID_RESPONSEThe response body was not JSON.
NETWORKThe request never reached the API. status is 0.
UNKNOWNNo code could be determined. status is 0.
app.js
try {
await OpenShop.cart.add({ slug: 'vip-perks' });
} catch (error) {
if (error.code === 'UNPROCESSABLE_ENTITY') return showBackInStockForm();
console.error(error.code, error.message);
}

The API follows the bundle’s semver.

Covered by semver — safe to build on:

  • Method names on OpenShop and its namespaces
  • Event names, including the os: document mirror
  • Snapshot field names and their types
  • data-os-* attribute names and behaviour
  • The os-components CSS cascade layer — its name, and that every style the bundle injects lives inside it

Not covered — do not build on these:

  • Internal class names such as .os-cart-item-name
  • The DOM structure inside a component
  • Svelte-scoped selectors

Matching on internal class names or component markup is how site code breaks on a routine refactor. If you are doing that today, this API is the replacement.

For styling, the os-components cascade layer means a plain class selector already beats ours, so you never need our generated hash classes to override a component. The class names you write that selector against are still ours to rename between versions — the layer makes overriding easy, not the class names permanent.

Deprecations are announced with a console.warn — logged once per page — for at least one minor release before anything is removed.

The build publishes a declaration file next to the bundle:

https://openshopgo.com/wc/openshop.d.ts

It declares OpenShop, every snapshot type, OpenShopEvent, and OpenShopError, and augments Window with OpenShop and OpenShopQ. Save it into your project and point at it with /// <reference path="./openshop.d.ts" /> at the top of a file to get completion and type checking in your editor.