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

# Website Tracking

> Track on-site product views, cart activity, and searches from any website with the Sequenzy browser SDK

Shopify stores get on-site tracking from the [storefront pixel](/integrations/shopify), installed for you when you connect the store. The **browser SDK** gives every other site the same events: custom storefronts, headless shops, marketplaces, booking and reservation sites, and SaaS marketing sites.

Once installed, these events arrive exactly like a native integration's, so the same automations work:

| Event                       | Fires when                      | Powers                       |
| --------------------------- | ------------------------------- | ---------------------------- |
| `product_viewed`            | A visitor opens a product page  | Browse abandonment, segments |
| `product_added_to_cart`     | A visitor adds an item          | Cart abandonment             |
| `product_removed_from_cart` | A visitor removes an item       | Cart snapshot upkeep         |
| `cart_viewed`               | A visitor opens the cart        | Cart abandonment             |
| `collection_viewed`         | A visitor opens a category page | Interest segments            |
| `search_submitted`          | A visitor searches              | Intent segments              |

## Anonymous visitors are not lost

Most people browse before they ever type an email address. The SDK keeps that history:

1. A visitor browses anonymously. Their events are held in their browser. The
   SDK still pings the endpoint so the dashboard's `lastUsedAt` shows the
   snippet is alive, but nothing is stored server-side for an anonymous
   visitor - the server answers `queued: false`.
2. The visitor signs in or reaches checkout. Your backend mints a short-lived
   identity token, and your site calls `sequenzy.identify(email, token)`.
3. The held events are replayed against that contact at their original timestamps.

So a shopper who browses three products and then signs in arrives with all three on their timeline, not just whatever they do afterwards.

Buffered history lives in the visitor's own browser, so it does not follow them to a different device or survive them clearing site data. If a different person identifies on the same browser, the buffer is discarded rather than replayed onto the wrong contact.

## Step 1: Create a tracking key

In the dashboard, go to **Settings → Integrations → Website tracking** and create a key. Or from the terminal:

```bash theme={null}
sequenzy web-tracking create \
  --name "Storefront" \
  --origins https://example.com,https://*.example.com
```

The key is **publishable**: it ships in your page source and anyone can read it.
That is by design, and it is why it can only append anonymous storefront events
— never read contacts or reach the rest of the API. An origin allowlist limits
browser misuse, but it is not identity proof because non-browser callers can
forge an `Origin` header. Identified events therefore also require a signed,
short-lived identity token minted by your backend.

<Note>
  A bare domain is read as `https`. `https://*.example.com` covers subdomains at
  any depth but not the apex, so list `https://example.com` separately if you
  need both. Add your development origin (for example `http://localhost:3000`)
  while you are wiring things up.
</Note>

## Step 2: Install the snippet

Paste the snippet from the dashboard or CLI into every page. Its first script
creates lightweight method stubs synchronously, so calls made while the SDK is
still downloading are queued rather than lost:

```html theme={null}
<script>
  (function (window) {
    const queue = (window.sequenzy = window.sequenzy || []);
    [
      "identify",
      "track",
      "viewedProduct",
      "addedToCart",
      "removedFromCart",
      "viewedCart",
      "viewedCollection",
      "searched",
      "reset",
    ].forEach((method) => {
      queue[method] =
        queue[method] ||
        function (...args) {
          queue.push([method, ...args]);
        };
    });
  })(window);
</script>
<script
  async
  src="https://api.sequenzy.com/sequenzy.js"
  data-sequenzy-key="seq_pk_your_key"
  data-sequenzy-company="your_workspace_id"
  data-sequenzy-endpoint="https://api.sequenzy.com"
></script>
```

The snippet embeds both the key and your workspace id, so use it as printed rather than rebuilding it - a wrong workspace id fails silently.

You can call the normal API immediately, including before the async loader
finishes:

```html theme={null}
<script>
  window.sequenzy.identify("buyer@example.com", identityToken);
</script>
```

Pre-load calls run in order as soon as the SDK loads.

### Install from npm

If your site has a JavaScript build, install the side-effect-free package and
create the client explicitly:

```bash theme={null}
npm install @sequenzy/web-sdk
```

```typescript theme={null}
import { createBrowserClient } from "@sequenzy/web-sdk";

const sequenzy = createBrowserClient({
  publicKey: "seq_pk_your_key",
  companyId: "your_workspace_id",
});
```

## Step 3: Identify your visitors

Call `identify` as early as your backend has authenticated someone - on sign-in,
and again at checkout. First mint a token from your backend using a secret API
key with `commerce:write`, `automations:trigger`, and `subscribers:write` (the
last one because minting can create the contact). Pass the same publishable
key that is in your snippet:

```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/web-tracking-identities" \
  -H "Authorization: Bearer YOUR_SECRET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "publicKey": "seq_pk_your_key",
    "email": "buyer@example.com"
  }'
```

Tokens last 24 hours by default (`ttlHours` accepts up to 720). Mint a fresh
one on each sign-in and at checkout rather than reaching for a long lifetime -
the token is a bearer proof, and a short expiry is what limits a leaked one.

Minting also establishes the email as a contact if it is not one yet, so
identified events always have a profile to land on. The contact is created
active with no list memberships and no automations triggered - enroll it
through your normal signup flows when you want more than event attribution.

Return only `identityToken` to that signed-in browser, then pass it with the
same normalized email:

```javascript theme={null}
sequenzy.identify("buyer@example.com", identityToken);
```

The token is bound to the workspace, tracking key, email, and expiry. A copied
publishable key or forged email cannot trigger sequences, segment changes, or
cart/browse recovery. Never expose the secret API key in browser code.

Call `reset()` on sign-out so the next person using that browser starts clean:

```javascript theme={null}
sequenzy.reset();
```

## Step 4: Track events

Each event has a convenience method:

```javascript theme={null}
sequenzy.viewedProduct({
  providerProductId: "SKU-123",
  title: "Protein Powder",
  imageUrl: "https://cdn.example.com/protein.jpg",
  url: "https://example.com/products/protein",
  priceCents: 8850,
  currency: "USD",
});

sequenzy.addedToCart({
  providerProductId: "SKU-123",
  title: "Protein Powder",
  quantity: 2,
  priceCents: 8850,
  currency: "USD",
});

sequenzy.viewedCart({
  cartId: "cart-1",
  currency: "USD",
  lineItems: [
    { providerProductId: "SKU-123", title: "Protein Powder", quantity: 2 },
  ],
});

sequenzy.viewedCollection({
  providerCollectionId: "supplements",
  title: "Supplements",
});

sequenzy.searched("protein");
```

`providerProductId` is your own product id. Use the same value you push to the [product catalog](/concepts/custom-commerce) so recommendations and product blocks in emails resolve to the right item.

<Note>
  Cart abandonment needs the cart contents, so include `quantity` on every cart
  event. Without it, a recovery email cannot say what was left behind.
</Note>

### When a cart disappears without telling us

The tracked cart is only as current as the events your site sends. Plenty of
sites empty a cart without ever firing a removal: a session-scoped cart timing
out, a reservation or inventory hold being released, the shopper finishing
checkout on another device, or the snippet simply not loading on a page.

So a tracked cart expires. After `expireAfterHours` (default 72) with no
activity, the cart is dropped instead of being emailed about, and a later add
starts a fresh cart rather than merging into the old items. That is what stops
a recovery email from listing things the shopper no longer has.

Sites using the browser SDK currently always use the default expiry.
`sequenzy shopify settings update --cart-expire-after-hours` applies per
connected Shopify store only - it has no effect on SDK-tracked carts.

The cleanest fix is still to tell us directly - call
`sequenzy.viewedCart({ ... })` with the current contents whenever your site
knows them, or fire `removedFromCart` when the cart empties. An explicit cart
view replaces the whole snapshot, so it corrects any drift in one call -
including `sequenzy.viewedCart({ lineItems: [] })`, which clears the tracked
cart so no abandonment email goes out for items the shopper no longer has.

## Configuration

Extra attributes on the script tag:

| Attribute                         | Default       | Purpose                                                            |
| --------------------------------- | ------------- | ------------------------------------------------------------------ |
| `data-sequenzy-site`              | Page hostname | Distinguishes storefronts when one workspace covers several brands |
| `data-sequenzy-identity-ttl-days` | `30`          | How long a browser keeps applying a stored identity                |
| `data-sequenzy-storage`           | on            | Set to `off` to keep everything in memory for the page load only   |

The stored identity only applies while its identity token is also unexpired, so with the default 24-hour token the browser stays identified for at most a day after the last `identify` call regardless of this setting. `data-sequenzy-identity-ttl-days` caps the window from the other side.

Lower `data-sequenzy-identity-ttl-days` on any site where one device is routinely shared between buyers rather than belonging to one of them - shared workstations, kiosks, in-store terminals, or any flow where people commonly buy on someone else's behalf. A stored identity means "this browser was last used by this person", not "this browser belongs to this person", and a long window on a shared machine can attribute one person's browsing to another.

Setting `data-sequenzy-storage="off"` disables the anonymous buffer as well, so visitors who identify later arrive with no prior history. Use it only where a consent policy requires it.

## Server-side alternative

If your visitors are signed in, you do not need the browser at all. Post the same events from your backend with your secret API key:

```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/webhooks/commerce/api/YOUR_COMPANY_ID/customer-events" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "eventName": "product_added_to_cart",
    "email": "buyer@example.com",
    "product": { "providerProductId": "SKU-123", "title": "Protein Powder" },
    "cartLine": { "providerProductId": "SKU-123", "quantity": 2 }
  }'
```

This is often the fastest way to get cart recovery live: it covers every signed-in shopper with no front-end work. The browser SDK then adds the visitors who browse before they sign in.

## Managing keys

<CodeGroup>
  ```bash CLI theme={null}
  sequenzy web-tracking list
  sequenzy web-tracking get wtk_123
  sequenzy web-tracking update wtk_123 --origins https://example.com
  sequenzy web-tracking update wtk_123 --deactivate
  sequenzy web-tracking delete wtk_123 --yes
  ```

  ```bash API theme={null}
  curl "https://api.sequenzy.com/api/v1/web-tracking-keys" \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```
</CodeGroup>

MCP exposes the same operations as `list_web_tracking_keys`, `get_web_tracking_key`, `create_web_tracking_key`, `update_web_tracking_key`, and `delete_web_tracking_key`. See the [MCP reference](/concepts/mcp).

Revoking a key stops its events within about a minute while keeping the key value visible, so you can still find the matching snippet on your site. Delete it once the snippet is gone.

## Troubleshooting

**No events arriving.** Check the key's `lastUsedAt`. If it is `null`, no event has successfully authenticated yet: the snippet may be missing or not loading, the page may not have fired an instrumented event, or the origin allowlist may be rejecting the request. Open your site's network tab and look for a POST to `/customer-events` and its response status.

**Events rejected with 403.** Either the page's origin is not on the key's
allowlist, or an identified event has a missing, expired, or mismatched identity
token. Note that `https://example.com` and `https://www.example.com` are
different origins, as are `http` and `https`.

**Anonymous history not appearing.** Confirm token minting succeeds and
`sequenzy.identify(email, identityToken)` runs at sign-in and checkout. Without
a valid proof, events stay anonymous and are never attached to a contact.

**Nothing works in an ad-blocked browser.** Some blockers drop third-party tracking scripts. Those visitors are covered by the server-side approach above if they sign in.
