GuidesWebsite tracking
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 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.
- 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. - An origin is the scheme, the host and the port, with no path and no wildcard, up to ten of them.
https://shop.example.comandhttps://www.shop.example.comare two origins. To test on your own machine, list your local address too, written the same way, withhttpand its port. - 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:
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:
<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, 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:
// 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:
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. |
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",
});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: [] });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
trackon each route change, not once when the script loads. - An
idempotency_keyafter the properties makes a resent event one event rather than two. Keys startingshopify:orwix:are reserved.
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,
httporhttps. Every link a shopper could click,url,checkout_url,cart_urland each item'surl, 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:
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. Pass the browser's rsk_aid cookie as anonymous_id so the order joins the same shopper's views and cart:
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"
}
]
}
}'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();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
emailorcontact_id, withanonymous_id, or with both. An address that is not a contact yet is kept and starts nothing. order_idis required. The other order events,store:order.paid,store:order.fulfilled,store:order.cancelledandstore:order.refunded, take the same shape.idempotency_keymakes a retried request one order rather than two, andoccurred_atback-dates an order by up to 90 days.start_flows: falserecords 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.
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"
}'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();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:
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.
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"
}
]
}'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", ... }]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. |
curl "https://api.rasket.com/stores/$STORE_ID/signals?status=open" \
-H "Authorization: Bearer $RASKET_API_KEY" \
-H "User-Agent: acme-billing/1.0"const { body } = await rasket.stores.listSignals(storeId, { status: "open" });
for (const signal of body.data) {
console.log(signal.kind, signal.product_id, signal.facts);
}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
factsare 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}/productsreads your catalogue back, oldest change first. Passupdated_sinceto 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.