> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aftersell.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Events

> Every Aftersell Cart SDK event: when each one fires, what it hands you, what to use it for, and the mistakes that cause infinite loops.

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

| Event                                         | Payload                                            | Fires when                                      |
| --------------------------------------------- | -------------------------------------------------- | ----------------------------------------------- |
| [`cart_loaded`](#cart_loaded)                 | [`AftersellCart`](/aftersell/cart/sdk-cart-object) | The cart loads, once per page.                  |
| [`cart_updated`](#cart_updated)               | [`AftersellCart`](/aftersell/cart/sdk-cart-object) | The cart contents change, after the first load. |
| [`item_added`](#item_added)                   | `{ item }`                                         | A new line appears in the cart.                 |
| [`item_removed`](#item_removed)               | `{ item }`                                         | A line disappears from the cart.                |
| [`cart_opened`](#cart_opened-and-cart_closed) | None                                               | The drawer opens.                               |
| [`cart_closed`](#cart_opened-and-cart_closed) | None                                               | The drawer closes.                              |
| [`checkout`](#checkout)                       | None                                               | The checkout button is clicked.                 |

## Subscribing

`events.on(event, handler)` registers a handler and **returns a function that unsubscribes it**:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
const off = window.aftersell.cart.events.on('cart_updated', (state) => {
  console.log('Cart total is now', state.totalPrice);
});

// later, to stop listening:
off();
```

* `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.

<Warning>
  **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.
</Warning>

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
// ❌ Loops forever: every add triggers an update, which triggers another add.
window.aftersell.cart.events.on('cart_updated', (state) => {
  window.aftersell.cart.actions.addItem(GIFT_VARIANT_ID, 1);
});

// ✅ Guarded: once the gift is present, the condition is false and it stops.
window.aftersell.cart.events.on('cart_updated', (state) => {
  const hasGift = state.items.some((line) => line.variantId === GIFT_VARIANT_ID);
  if (state.totalPrice >= 5000 && !hasGift) {
    window.aftersell.cart.actions.addItem(GIFT_VARIANT_ID, 1);
  }
});
```

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.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
// ❌ Corrupts the payload for every later handler.
window.aftersell.cart.events.on('cart_updated', (state) => {
  state.items = state.items.filter((line) => line.finalLinePrice > 0);
});

// ✅ Copy first.
window.aftersell.cart.events.on('cart_updated', (state) => {
  const paidItems = state.items.filter((line) => line.finalLinePrice > 0);
});
```

To actually change the cart, use an [action](/aftersell/cart/sdk-actions). To change how lines render, use [`registerLineTransform`](/aftersell/cart/sdk-hooks#registerlinetransform).

***

## cart\_loaded

Fires **once**, when the cart first loads on the page. The payload is the full [cart object](/aftersell/cart/sdk-cart-object).

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('cart_loaded', (state) => {
  console.log('Page loaded with', state.itemCount, 'items');
});
```

**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.

<Tip>
  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".
</Tip>

## 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](/aftersell/cart/sdk-cart-object).

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('cart_updated', (state) => {
  document.querySelector('#my-total').textContent =
    window.aftersell.cart.actions.formatMoney(state.totalPrice);
});
```

**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.

<Warning>
  Re-read [the two rules](#the-two-rules) before calling an action in here.
</Warning>

## item\_added

Fires when a **new line** appears in the cart. The payload is `{ item }`, where `item` is the [cart line](/aftersell/cart/sdk-cart-object#cart-lines).

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('item_added', (payload) => {
  myAnalytics.track('Added to cart', {
    id: payload.item.variantId,
    title: payload.item.title,
    quantity: payload.item.quantity,
  });
});
```

**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](/aftersell/cart/sdk-use-case-analytics).

Two things to know about how it's derived:

<Warning>
  **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.
</Warning>

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`.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('item_removed', (payload) => {
  console.log('Removed', payload.item.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.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('cart_opened', () => {
  myAnalytics.track('Cart viewed');
});

window.aftersell.cart.events.on('cart_closed', () => {
  document.body.classList.remove('cart-is-open');
});
```

**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.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.aftersell.cart.events.on('checkout', () => {
  myAnalytics.track('Checkout started');
});
```

**Use it for:** checkout-intent tracking.

<Warning>
  **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`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon) for anything you need to reliably send.
</Warning>

***

## 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.

| Bus event      | DOM event                     |
| -------------- | ----------------------------- |
| `cart_loaded`  | `aftersell:cart:cart-loaded`  |
| `cart_updated` | `aftersell:cart:cart-updated` |
| `item_added`   | `aftersell:cart:item-added`   |
| `item_removed` | `aftersell:cart:item-removed` |
| `cart_opened`  | `aftersell:cart:cart-opened`  |
| `cart_closed`  | `aftersell:cart:cart-closed`  |
| `checkout`     | `aftersell:cart:checkout`     |

Mind the naming: the bus uses `snake_case`, the DOM events use `kebab-case` behind an `aftersell:cart:` prefix.

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
window.addEventListener('aftersell:cart:cart-updated', (event) => {
  console.log('Cart total is now', event.detail.totalPrice);
});
```

The payload arrives on `event.detail` and matches the [cart object](/aftersell/cart/sdk-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.

<Warning>
  **`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`.
</Warning>

### Shopify standard cart events

Separately, the cart publishes Shopify's [standard cart events](https://shopify.dev/docs/storefronts/themes/best-practices/standard-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:

| Event                          | Payload on the event instance                                                    |
| ------------------------------ | -------------------------------------------------------------------------------- |
| `shopify:cart:lines-update`    | `action: 'add' \| 'update' \| 'remove'`, `context: 'cart' \| 'product'`, `lines` |
| `shopify:cart:note-update`     | `context: 'cart'`, `note`                                                        |
| `shopify:cart:discount-update` | `discountCodes: [{ code }]`                                                      |

<Warning>
  **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`.
</Warning>

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](/aftersell/cart/sdk-cart-object)**: the full shape of the payloads above.
* **[Actions](/aftersell/cart/sdk-actions)**: how to change the cart from a handler.
* **[Hooks](/aftersell/cart/sdk-hooks)**: for changing how the cart renders, rather than reacting to it.
* **[Use cases](/aftersell/cart/sdk-use-cases)**: analytics tracking, free gifts, and other complete examples.
