Skip to main content
Events let you run code when something happens in the cart. They live under window.aftersell.cart.events. Subscribing is a set-up call, so it’s safe at the top of your script, with no need to wait for ready().

Available events

Subscribing

events.on(event, handler) registers a handler and returns a function that unsubscribes it:
  • events.once(event, handler): fires once, then unsubscribes itself.
  • events.off(event, handler): removes a specific handler.
A handler that throws is isolated and logged to the console; the other handlers still run.

The two rules

Almost every event bug traces back to one of these.

Don’t change the cart from cart_updated without a guard

Changing the cart inside a cart_updated handler fires cart_updated again. If that handler changes the cart again, you have an infinite loop. The shopper watches their cart thrash while the page hammers Shopify.
Never call an action unconditionally from cart_updated or cart_loaded. Guard it with a check on the state you’re about to create, so the second pass does nothing.
The cart does give you one safety net: an update that produces an identical cart emits nothing, so a refetch that changes nothing won’t restart the cycle. That protects you from accidental no-op loops. It does not protect you from a handler that genuinely changes the cart each time.

Treat the payload as read-only

Every handler for one event receives the same object. Mutating it changes what the handlers after yours see, including handlers belonging to other apps on the store.
To actually change the cart, use an action. To change how lines render, use registerLineTransform.

cart_loaded

Fires once, when the cart first loads on the page. The payload is the full cart object.
Use it for: anything that needs to run against the cart’s starting state, such as reconciling a free gift, initializing a widget, or reporting cart contents to analytics on page load. cart_loaded is replayed to late subscribers. If you subscribe after the cart has already loaded, your handler is called immediately with the current cart. Subscription order never matters, so you don’t have to worry about whether your script beat the cart.
Logic that has to be correct both on page load and on every change afterward should subscribe to both cart_loaded and cart_updated with the same function. That’s the standard pattern for “keep X in sync with the cart”.

cart_updated

Fires every time the cart contents change after the first load, whether from the drawer, from your own actions, from the theme, or from another app. The payload is the full cart object.
Use it for: keeping something outside the cart in sync, such as a custom total, a progress bar, a header badge, or an analytics event on every change. An update that produces an identical cart emits nothing. Re-opening the drawer, switching tabs back, or a refetch that returns the same contents will not fire it.
Re-read the two rules before calling an action in here.

item_added

Fires when a new line appears in the cart. The payload is { item }, where item is the cart line.
Use it for: add-to-cart tracking in a third-party analytics tool. This is the single most common use of the SDK. See tracking add-to-cart. Two things to know about how it’s derived:
A quantity change is not an add. The cart works out adds and removes by diffing lines, not quantities. A shopper bumping a line from 1 to 3 fires cart_updated, not item_added. If you need to catch quantity increases too, compare against the previous state in a cart_updated handler.
It also doesn’t fire for items that were already in the cart when the page loaded; those arrive via cart_loaded. Adding several distinct products at once fires the event once per line.

item_removed

Fires when a line disappears from the cart. The payload is { item }, the line as it was just before it went away, so you can still read its key, variantId, and title.
Use it for: reversing something you did on add, such as clearing a flag, re-showing an offer the shopper declined, or reporting removals to analytics. Same caveat as item_added: lowering a quantity without hitting zero isn’t a removal.

cart_opened and cart_closed

Fire when the drawer opens and closes. No payload.
Use it for: view tracking, pausing a video or carousel behind the drawer, toggling a class on the page. Neither fires on the initial page load, only on an actual open or close.

checkout

Fires when the shopper clicks the checkout button, immediately before the browser navigates. No payload.
Use it for: checkout-intent tracking.
You can’t cancel checkout from this handler. The event is a notification, not a gate; navigation happens regardless of what your code does. Keep the handler fast and synchronous: an await or a slow network call may not finish before the page unloads. Use navigator.sendBeacon for anything you need to reliably send.

Listening from outside the SDK

Every event is also dispatched as a DOM CustomEvent on window, so you can listen without touching window.aftersell.cart. That is useful from a theme file, a third-party app, or a script that loads independently of the cart. Mind the naming: the bus uses snake_case, the DOM events use kebab-case behind an aftersell:cart: prefix.
The payload arrives on event.detail and matches the cart object. Events are dispatched on window, so a listener anywhere on the page receives them. The cart renders in a shadow root, but the shadow boundary is never in the event’s path. Each dispatch clones the payload, so a listener mutating event.detail can’t affect anyone else, and a listener that throws can’t disrupt the SDK.
cart-loaded does not replay on the DOM. The bus replays cart_loaded to late subscribers, but that path bypasses the DOM dispatch, so window.addEventListener('aftersell:cart:cart-loaded') registered after the cart has already loaded will never fire. If your script’s load order isn’t guaranteed, use window.aftersell.cart.events.on('cart_loaded', …), which does replay, or also listen for aftersell:cart:cart-updated.

Shopify standard cart events

Separately, the cart publishes Shopify’s standard cart events on document whenever it changes the cart, so theme code and other apps can react to Aftersell’s mutations the same way they react to the theme’s:
The payload is not on event.detail. detail carries only { source: 'aftersell' } — the tag the cart uses to ignore its own events instead of looping. Everything in the table above is assigned directly onto the event object, so read event.action, not event.detail.action.
Each event also carries a promise that Aftersell settles when the underlying write lands, matching Shopify’s standard — await it, don’t resolve it. These are dispatched on document and bubble, so a window listener receives them too.

Where to go next

  • Cart object: the full shape of the payloads above.
  • Actions: how to change the cart from a handler.
  • Hooks: for changing how the cart renders, rather than reacting to it.
  • Use cases: analytics tracking, free gifts, and other complete examples.