# Website tracking with rasket.js

Send product views, cart updates and checkouts from your own website, so an abandoned-cart or browse flow can start from them. One script in your pages' head, one consent call from your cookie banner, and one line of code per event. Orders, and who each shopper is, come from your server.

## Before you start

On Shopify, install the [Rasket app](https://www.rasket.com/docs/integrations/shopify) instead: it sends these events, checkouts and orders for you, and nothing on this page applies. This page is for a site you build yourself.

1. In Rasket, open **Automations → Events** and, under **Website tracking**, add your website: a name, and every origin the script runs on, one per line, such as `https://shop.example.com`.
2. An origin is the scheme, the host and the port, with no path and no wildcard, up to ten of them. `https://shop.example.com` and `https://www.shop.example.com` are two origins. To test on your own machine, list your local address too, written the same way, with `http` and its port.
3. The website gets a publishable key, `rk_pub_…`. It is meant for your HTML: it can only send the three events below, and only from the origins you listed.

To change the list later, send the whole new list with a full-access API key. The key and everything Rasket has seen from the site stay as they are:

Change a website's origins:

```text
PATCH /stores/{store_id}

{
  "name": "Shop",
  "allowed_origins": ["https://shop.example.com", "https://www.shop.example.com"]
}
```

The website's ID is in `GET /stores`. Adding the website a second time instead gives it a new key and starts its check from nothing.

## 1. Add the snippet to every page's head

Copy the snippet from the card, with your key in it, into the `<head>` of every page:

The tracking snippet:

```text
<script>
  window.rasket = window.rasket || function () { (rasket.q = rasket.q || []).push(arguments); };
  rasket("init", { key: "rk_pub_…", consent: "required" });
</script>
<script async src="https://www.rasket.com/rasket.js"></script>
```

The first line queues calls until the script loads, so `rasket(…)` works anywhere on the page after the snippet. Load the script from exactly the address above, with `www`: rasket.js sends events back to the origin it was loaded from, and from any other address they are refused.

Keep the snippet before any other `rasket(…)` call: its `init` starts the script, and only the first `init` on a page counts. If the page already starts rasket.js for [sign-up forms](https://www.rasket.com/docs/forms), keep that one and add to it what this page asks of `init`.

## 2. Connect your cookie banner

With `consent: "required"`, rasket.js sends nothing and writes no cookie until your banner says the visitor agreed to both analytics and marketing:

The consent call:

```text
// In your banner tool's "accept" callback, once the visitor allows analytics and marketing:
rasket("consent", { analytics: true, marketing: true });

// If they withdraw later. The rsk_aid cookie is deleted and nothing more is sent:
rasket("consent", { analytics: false, marketing: false });
```

Until then, `track` and `identify` calls are dropped, not saved for later, and agreeing to only one of the two is a no. So call `consent` on every page load, before the page tracks anything: from your banner tool's callback when the visitor answers, and from the one that runs when a stored answer is read back.

If your site has no banner to wait for, say so in `init` and events are sent from the first page:

Without a cookie banner:

```text
rasket("init", { key: "rk_pub_…", consent: "granted" });
```

> Whether you need a banner is your decision, not Rasket's. Once events may be sent, rasket.js writes one first-party cookie, `rsk_aid`: a random ID for this browser, kept for a year. It holds nothing about the visitor.

## 3. Send events from your pages

A browser can send three events. Each needs one field; everything else is optional but makes better emails.

| Event | Required | Send it when |
| --- | --- | --- |
| `product.viewed` | `product_id` | A shopper opens a product page. |
| `cart.updated` | `items` | The cart changes: an item added, removed or its quantity changed. An empty list means the cart was emptied. |
| `checkout.started` | `checkout_url` | The shopper reaches your checkout. The URL is the link a reminder email sends them back to. |

A product view:

```text
rasket("track", "product.viewed", {
  product_id: "red-tee",
  title: "Red tee",
  url: "https://shop.example.com/products/red-tee",
  image_url: "https://cdn.example.com/red-tee.jpg",
  price: "25.00",
  currency: "USD",
});
```

A cart update:

```text
rasket("track", "cart.updated", {
  value: "50.00",
  currency: "USD",
  cart_url: "https://shop.example.com/cart",
  items: [
    {
      title: "Red tee",
      variant: "Medium",
      quantity: 2,
      price: "25.00",
      product_id: "red-tee",
      url: "https://shop.example.com/products/red-tee",
      image_url: "https://cdn.example.com/red-tee.jpg",
    },
  ],
});

// The cart was emptied: still send it, with no items.
rasket("track", "cart.updated", { items: [] });
```

A checkout:

```text
rasket("track", "checkout.started", {
  checkout_url: "https://shop.example.com/checkout/c-81f2",
  value: "50.00",
  currency: "USD",
  items: [{ title: "Red tee", quantity: 2, price: "25.00" }],
});
```

- The `store:` prefix is optional: `"cart.updated"` and `"store:cart.updated"` are the same event.
- A site that changes pages without reloading calls `track` on each route change, not once when the script loads.
- An `idempotency_key` after the properties makes a resent event one event rather than two. Keys starting `shopify:` or `wix:` are reserved.

An event sent once:

```text
rasket("track", "checkout.started", { checkout_url: "https://shop.example.com/checkout/c-81f2" }, {
  idempotency_key: "checkout-c-81f2",
});
```

| Key | What it holds |
| --- | --- |
| `value` | The cart, checkout or order total, as a decimal string. Needs currency. |
| `currency` | Three capital letters, like USD or EUR. |
| `product_id` | Your own ID for the product: up to 255 characters, or a whole number. |
| `title` | The product's name, up to 500 characters. |
| `price` | One unit's price, as a decimal string. |
| `url` | The product page. |
| `image_url` | The product picture, on any host, such as your CDN. |
| `checkout_url` | The checkout page. |
| `cart_url` | A link that rebuilds the cart. |
| `cart_token` | Your own ID for the cart. |
| `checkout_token` | Your own ID for the checkout. |
| `checkout_id` | Another ID for the checkout, if your platform has two. |
| `order_id` | Your ID for the order. Orders only, from your server. |
| `order_number` | The order number the shopper sees, such as #1042. |
| `discount_code` | A code the shopper applied. |
| `items` | Up to 50 lines, each with title, quantity (a whole number from 1 to 10,000) and price, and optionally variant, product_id, variant_id, url and image_url. |

- Money is a decimal string that is not negative, with at most four decimals and no symbol or thousands separator: `"1299.00"`, not `"$1,299.00"`. A number is accepted and stored as a decimal string.
- URLs are absolute, `http` or `https`. Every link a shopper could click, `url`, `checkout_url`, `cart_url` and each item's `url`, must be on one of your website's origins, so nobody can put another site's link in your emails. Images may live anywhere.
- A key that is not in the table refuses the whole event, so a typo shows up on the check instead of quietly missing from your emails.
- An event is at most 16 KB.

## 4. Tell Rasket who the shopper is

When a visitor who has agreed types their email, in a newsletter box or at checkout, say so:

Identify from the browser:

```text
rasket("identify", { email: "maya@example.com" });
```

From the browser this is only a claim: anyone can type an address. It is kept with the browser and ties it to nobody. The browser's events reach a contact, and can start a flow for them, only once your server has confirmed who it is (below), and only the events from then on. No contact is ever created by an event.

## 5. Orders and sign-ins from your server

An order is a fact only your server can state, so it is sent with an API key, through [Send an event](https://www.rasket.com/docs/api-reference/events). Pass the browser's `rsk_aid` cookie as `anonymous_id` so the order joins the same shopper's views and cart:

Send an order:

```sh
curl -X POST "https://api.rasket.com/events/send" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "event": "store:order.placed",
  "email": "maya@example.com",
  "anonymous_id": "4f6c2a7e-0b1d-4c7a-9a52-1f3e8d6b2c90",
  "idempotency_key": "order-1042",
  "payload": {
    "order_id": "1042",
    "order_number": "#1042",
    "value": "50.00",
    "currency": "USD",
    "items": [
      {
        "title": "Red tee",
        "quantity": 2,
        "price": "25.00"
      }
    ]
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/events/send", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    event: "store:order.placed",
    email: "maya@example.com",
    anonymous_id: "4f6c2a7e-0b1d-4c7a-9a52-1f3e8d6b2c90",
    idempotency_key: "order-1042",
    payload: {
      order_id: "1042",
      order_number: "#1042",
      value: "50.00",
      currency: "USD",
      items: [
        {
          title: "Red tee",
          quantity: 2,
          price: "25.00"
        }
      ]
    }
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/events/send",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "event": "store:order.placed",
    "email": "maya@example.com",
    "anonymous_id": "4f6c2a7e-0b1d-4c7a-9a52-1f3e8d6b2c90",
    "idempotency_key": "order-1042",
    "payload": {
      "order_id": "1042",
      "order_number": "#1042",
      "value": "50.00",
      "currency": "USD",
      "items": [
        {
          "title": "Red tee",
          "quantity": 2,
          "price": "25.00"
        }
      ]
    }
  },
)

print(response.json())
```

- Say who ordered with `email` or `contact_id`, with `anonymous_id`, or with both. An address that is not a contact yet is kept and starts nothing.
- `order_id` is required. The other order events, `store:order.paid`, `store:order.fulfilled`, `store:order.cancelled` and `store:order.refunded`, take the same shape.
- `idempotency_key` makes a retried request one order rather than two, and `occurred_at` back-dates an order by up to 90 days.
- `start_flows: false` records an order without starting any flow, for importing past orders.
- An order sent with an API key belongs to your team rather than to one website.

When someone signs in or checks out, tie their browser to them. From then on the browser's events reach that contact. Earlier events are not replayed.

Identify a visitor:

```sh
curl -X POST "https://api.rasket.com/events/identify" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "anonymous_id": "4f6c2a7e-0b1d-4c7a-9a52-1f3e8d6b2c90",
  "email": "maya@example.com"
}'
```

```ts
const response = await fetch("https://api.rasket.com/events/identify", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    anonymous_id: "4f6c2a7e-0b1d-4c7a-9a52-1f3e8d6b2c90",
    email: "maya@example.com"
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/events/identify",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "anonymous_id": "4f6c2a7e-0b1d-4c7a-9a52-1f3e8d6b2c90",
    "email": "maya@example.com"
  },
)

print(response.json())
```

Your server reads `rsk_aid` from the request's cookies when it runs on the same host as the pages: the cookie belongs to that exact host, so a server on another subdomain does not receive it. In the browser, ask for it and pass it along:

Read the anonymous ID:

```text
rasket("anonymousId", function (id) {
  // null until the visitor has agreed; otherwise the rsk_aid value.
});
```

## 6. Check your installation

Back on the Website tracking card, press **Check installation**. Four rows say what Rasket has seen from the site:

| Row | Turns green when |
| --- | --- |
| Snippet installed | Rasket found rasket.js with this website's key in your home page, or an event from the site has already arrived. A tag manager adds the script after the page loads, so the second half is what turns the row green there. |
| Product views | A product.viewed event from the site was accepted. |
| Cart updates | A cart.updated event from the site was accepted. |
| Checkouts | A checkout.started event from the site was accepted. |

Each row carries a short note:

- **Found on shop.example.com**: the snippet, with this website's key, is in your home page.
- **Received 2 min ago**: the last accepted event of that kind.
- **None yet**: nothing has arrived. Open a page that sends the event, in a browser where you accepted your own banner, and check again.
- **Rejected: …**: the last event of that kind was refused, with the reason. Fix the call and send it again; the row turns green on the next accepted one.
- **Not found on shop.example.com**: the snippet is not in the home page's HTML and no event has arrived yet.

A disconnected website is not checked, and its rows say **Disconnected**.

The card's badge reads **Not verified** until a row passes, **Partly connected** while some do, and **Connected** when all four do.

rasket.js never reads Rasket's reply, so a refusal makes no noise on your page. A row that stays on None yet usually means the request was turned away before Rasket accepted it as your website's. The browser then reports a CORS error for the `/api/storefront/events` request and hides the reply, and the cause is one of these:

| Status | Cause |
| --- | --- |
| 403 | The page's origin is not in the website's list, exactly: https://shop.example.com and https://www.shop.example.com are two origins. |
| 401 | The key in init is mistyped, the website was disconnected, or its key was rotated. Rotate key ends the old key at once: paste the new snippet everywhere the old one is. |
| 413 | The request is larger than 16 KB. Send fewer items, or shorter titles. |
| None | The script was loaded from another address than the snippet's, so its events go somewhere that does not know your website. |

Once Rasket knows the website, a refusal comes back as `{ "ok": false, "error": "…" }`, which your browser's network tab shows:

| Status | Reply | What to do |
| --- | --- | --- |
| 422 | payload.product_id: store:product.viewed requires product_id. | A required field is missing, or a field is not one of the keys above. The check shows it as Rejected. |
| 422 | … is not on an allowed origin of this key. | A url, checkout_url, cart_url or item url points at another site. |
| 422 | A browser may send store:product.viewed, store:cart.updated, store:checkout.started. Send orders from your server. | Orders come from your server with an API key. |
| 422 | idempotency_key may not start with shopify:. | The shopify: and wix: prefixes are reserved. Pick another key. |
| 429 | Too many events from this visitor. | More than 20 events in ten seconds from one IP address. Send an event when something changes, not on a timer. |
| 429 | Too many events for this site. | More than 200 events in ten seconds from the whole website. |

## 7. Your products and store signals

Send Rasket your catalogue so emails can show your products and Autopilot can see what changed. Push up to 500 products at a time with a full-access API key, using the same `product_id` your rasket.js events send. Push a product again whenever it changes; one Rasket already holds with a newer `updated_at` is left as it is.

Push your products:

```sh
curl -X PUT "https://api.rasket.com/stores/$STORE_ID/products" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
    "products": [
      {
        "product_id": "red-tee",
        "title": "Red tee",
        "url": "https://shop.example.com/products/red-tee",
        "image_url": "https://cdn.example.com/red-tee.jpg",
        "price": "19.00",
        "compare_at_price": "25.00",
        "currency": "USD",
        "available": true,
        "inventory_quantity": 4,
        "updated_at": "2026-10-25T09:12:00Z"
      }
    ]
  }'
```

```ts
import { Rasket } from "rasket";

const apiKey = process.env.RASKET_API_KEY;
if (!apiKey) throw new Error("RASKET_API_KEY is not set");

const rasket = new Rasket({ apiKey, userAgent: "acme-billing/1.0" });

const { body } = await rasket.stores.pushProducts(storeId, {
  products: [
    {
      product_id: "red-tee",
      title: "Red tee",
      url: "https://shop.example.com/products/red-tee",
      description: "",
      image_url: "https://cdn.example.com/red-tee.jpg",
      status: "active",
      available: true,
      price: "19.00",
      compare_at_price: "25.00",
      currency: "USD",
      inventory_quantity: 4,
      updated_at: "2026-10-25T09:12:00Z",
      variants: [],
    },
  ],
});
console.log(body.data); // [{ index: 0, status: "written", ... }]
```

```python
import os

from rasket import Rasket

rasket = Rasket(
    api_key=os.environ["RASKET_API_KEY"],
    user_agent="acme-billing/1.0",
)

result = rasket.stores.push_products(store_id, {
    "products": [
        {
            "product_id": "red-tee",
            "title": "Red tee",
            "url": "https://shop.example.com/products/red-tee",
            "description": "",
            "image_url": "https://cdn.example.com/red-tee.jpg",
            "status": "active",
            "available": True,
            "price": "19.00",
            "compare_at_price": "25.00",
            "currency": "USD",
            "inventory_quantity": 4,
            "updated_at": "2026-10-25T09:12:00Z",
            "variants": [],
        }
    ]
})
print(result.body["data"])
```

Rasket compares each push with what it holds. Your first push is the baseline and records nothing, nor do the products first seen in the day after it, so a large first import is never a flood. After that, a change worth a campaign becomes a store signal:

| Signal | When |
| --- | --- |
| `product_new` | A product went live after your first push. |
| `collection_new` | A drop: three or more new products within 48 hours. Its products are in facts.product_ids. |
| `price_drop` | The price fell 5% or more below the last price that held for a day, or a compare-at price above it appeared. |
| `back_in_stock` | Buyable again after a day or more out of stock. |
| `low_stock` | Five or fewer left, when you send inventory_quantity. |
| `trending` | Ordered at twice its usual pace this week, in five or more orders. |

Read open signals:

```sh
curl "https://api.rasket.com/stores/$STORE_ID/signals?status=open" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const { body } = await rasket.stores.listSignals(storeId, { status: "open" });

for (const signal of body.data) {
  console.log(signal.kind, signal.product_id, signal.facts);
}
```

```python
signals = rasket.stores.list_signals(store_id, {"status": "open"})

for signal in signals.body["data"]:
    print(signal["kind"], signal["product_id"], signal["facts"])
```

- A signal's `facts` are what the catalogue saw: the product and its price, availability and stock before and after. Every number comes from your push.
- One signal per product, kind and week. Pushing the same change again records nothing.
- Open signals expire after three to fourteen days, by kind, and are deleted after 90.
- `GET /stores/{store_id}/products` reads your catalogue back, oldest change first. Pass `updated_since` to fetch only what changed.

## Locked triggers

In an automation's trigger picker, a store trigger whose event has not passed the check cannot be chosen yet, and says what it is waiting for, such as **Needs cart updates**, or **Needs order events** for an order trigger. It unlocks once Rasket has accepted that event, and stays unlocked.

- A store connected through a platform app, such as Shopify, unlocks every store trigger: the app sends the events itself.
- An automation that already uses a store trigger keeps running.
