Install RecartIQ
Four steps. Traffic and friction come from the snippet; orders you send, because no script can see them on its own. Pick your platform.
1.Add the snippet to your global header template
<script>window.riq=window.riq||function(){(riq.q=riq.q||[]).push(arguments)};</script>
<script async src="https://cdn.recartiq.com/r.js" data-key="pk_..."></script>Both lines, in that order. The first is a queue: the snippet loads asynchronously so it does not slow your store down, and without the queue a call made before it finishes throws and the event is lost. Your key is on the Install page of the store and in Settings → API keys.
That alone gives you page views, sessions, device, country and UTM attribution. Single-page apps get a page view on every route change. Empty searches and checkout or coupon errors are picked up too, where your pages expose them: a path containing cart or checkout, and the error rendered in an element with role="alert".
Out-of-stock views, size-guide opens and product views are not detected on a custom site. Those need theme markup we recognise, so send them with the calls in step 2.
2.Send the commerce events
Call these wherever the thing happens: the product page, the add-to-cart handler, the checkout button.
riq("track", "view_product", { product_id: "SKU-1", name: "Trail Jacket", price: 129, currency: "USD" });
riq("track", "add_to_cart", { product_id: "SKU-1", price: 129, currency: "USD", quantity: 1, cart_value: 129 });
riq("track", "begin_checkout", { cart_value: 129, currency: "USD" });
riq("identify", "customer-123", { email: "c@example.com" });No JavaScript to hand? Annotate the button instead:
<button data-riq-event="add_to_cart" data-riq-product-id="SKU-1" data-riq-price="129" data-riq-currency="USD">Add to cart</button>
3.Send the purchase
You own the checkout, so call it on your order confirmation page like any other event. It carries the revenue behind conversion rate, AOV and the value of every product leak, and nothing detects it for you.
riq("track", "purchase", { order_id: "1001", revenue: 137.9, currency: "USD", shipping: 8.9,
items: [{ product_id: "SKU-1", price: 129, quantity: 1 }] });If the confirmation page can be reloaded, send it from your backend with the HTTP API below instead, so one order is counted once.
4.Check it arrived
The Install page of each store watches for the first event and lists which standard events it has seen, so a half-finished install is obvious. Live view shows events landing as they happen. An event missing a required property is rejected and named in the response.
1.Add the snippet to your theme
Online Store → Themes → Edit code → layout/theme.liquid, before </head>.
<script>window.riq=window.riq||function(){(riq.q=riq.q||[]).push(arguments)};</script>
<script async src="https://cdn.recartiq.com/r.js" data-key="pk_..."></script>Both lines, in that order. The first is a queue: the snippet loads asynchronously so it does not slow your store down, and without the queue a call made before it finishes throws and the event is lost. Your key is on the Install page of the store and in Settings → API keys.
That alone gives you page views, sessions, device, country and UTM attribution, plus product views, out-of-stock views, empty searches and size-guide opens, which are all detected from the Shopify theme. Coupon and checkout errors are caught on your cart page, but not inside Shopify's hosted checkout, which no theme script can reach.
2.Add the checkout pixel
Shopify hosts its own checkout and your theme never loads there, so no snippet can see the order. A pixel does run there. In the Shopify admin go to Settings → Customer events → Add custom pixel, name it RecartIQ, paste this and save. No server, and nothing else to wire up.
// RecartIQ, Shopify Customer Events pixel.
// Sends add to cart, checkout start and the order. Page and product views come
// from the snippet in your theme, so they are not repeated here.
(function () {
var URL = "https://cdn.recartiq.com/api/v1/track" + "?k=" + "pk_...";
// A stable id derived from the order, so a repeated delivery is deduplicated
// rather than counted twice.
function uuidFrom(s) {
var a = 0x811c9dc5, b = 0x01000193;
for (var i = 0; i < s.length; i++) {
a = ((a ^ s.charCodeAt(i)) * 16777619) >>> 0;
b = ((b << 5) - b + s.charCodeAt(i)) >>> 0;
}
var hex = (("00000000" + a.toString(16)).slice(-8) + ("00000000" + b.toString(16)).slice(-8)).repeat(2);
return hex.slice(0, 8) + "-" + hex.slice(8, 12) + "-4" + hex.slice(13, 16) + "-a" + hex.slice(17, 20) + "-" + hex.slice(20, 32);
}
var ids;
function identity() {
if (!ids) {
var read = function (n) { return browser.cookie.get(n).then(function (v) { return v || ""; }).catch(function () { return ""; }); };
ids = Promise.all([read("riq_aid"), read("riq_sid")]).then(function (v) { return { aid: v[0], sid: v[1] }; });
}
return ids;
}
function send(name, properties, event, dedupe, distinctId) {
identity().then(function (id) {
if (!id.aid && !distinctId) return; // nothing to attach it to
var doc = (event && event.context && event.context.document) || {};
var loc = doc.location || {};
var e = {
event: name,
timestamp: new Date(event && event.timestamp ? event.timestamp : Date.now()).toISOString(),
properties: properties,
context: { url: loc.href, path: loc.pathname, referrer: doc.referrer, session_id: id.sid || undefined, sdk: { name: "shopify-pixel", version: "1" } }
};
if (id.aid) e.anonymous_id = id.aid;
if (distinctId) e.distinct_id = distinctId;
if (dedupe) e.insert_id = uuidFrom(dedupe);
fetch(URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ batch: [e] }), keepalive: true }).catch(function () {});
});
}
var num = function (v) { return typeof v === "number" ? v : parseFloat(v || 0) || 0; };
function item(variant, quantity) {
var p = (variant && variant.price) || {};
return {
product_id: String((variant && variant.product && variant.product.id) || (variant && variant.id) || ""),
sku: (variant && variant.sku) || undefined,
name: (variant && variant.product && variant.product.title) || undefined,
variant: (variant && variant.title) || undefined,
price: num(p.amount),
quantity: Math.max(1, Math.round(num(quantity) || 1))
};
}
analytics.subscribe("product_added_to_cart", function (event) {
var line = event.data && event.data.cartLine;
var m = line && line.merchandise;
var cur = m && m.price && m.price.currencyCode;
if (!m || !cur) return;
var it = item(m, line.quantity);
if (!it.product_id) return;
send("add_to_cart", { product_id: it.product_id, sku: it.sku, name: it.name, variant: it.variant, price: it.price, currency: cur, quantity: it.quantity }, event);
});
analytics.subscribe("checkout_started", function (event) {
var c = event.data && event.data.checkout;
var t = c && c.totalPrice;
if (!c || !t || !t.currencyCode) return;
var lines = c.lineItems || [];
send("begin_checkout", {
cart_value: num(t.amount),
currency: t.currencyCode,
cart_item_count: lines.length,
items: lines.map(function (l) { return item(l.variant, l.quantity); })
}, event);
});
analytics.subscribe("checkout_completed", function (event) {
var c = event.data && event.data.checkout;
var t = c && c.totalPrice;
var orderId = c && c.order && c.order.id;
if (!c || !t || !t.currencyCode || !orderId) return;
var lines = c.lineItems || [];
send(
"purchase",
{
order_id: String(orderId),
revenue: num(t.amount),
currency: t.currencyCode,
items: lines.map(function (l) { return item(l.variant, l.quantity); })
},
event,
"order:" + orderId,
c.email || undefined
);
});
})();
It reads the same cookies the snippet set while the shopper was browsing, so the order lands in that visitor's session and completes the funnel rather than arriving anonymous. Do not use the old Additional Scripts box on the order status page: Shopify is removing it.
That is the whole install. Between the snippet and the pixel, page views, product views, add to cart, checkout start, the order, out of stock views, empty searches and size-guide opens all arrive on their own. Nothing below is required.
3.Optional: extra events
Only worth adding if you want something Shopify does not expose. identify ties a signed-in shopper to their orders across devices; the rest give richer properties than the automatic capture.
riq("track", "view_product", { product_id: "SKU-1", name: "Trail Jacket", price: 129, currency: "USD" });
riq("track", "add_to_cart", { product_id: "SKU-1", price: 129, currency: "USD", quantity: 1, cart_value: 129 });
riq("track", "begin_checkout", { cart_value: 129, currency: "USD" });
riq("identify", "customer-123", { email: "c@example.com" });No JavaScript to hand? Annotate the button instead:
<button data-riq-event="add_to_cart" data-riq-product-id="SKU-1" data-riq-price="129" data-riq-currency="USD">Add to cart</button>
Coupon errors and checkout errors are captured on your cart page but not inside Shopify's hosted checkout, which the theme cannot reach.
4.Check it arrived
The Install page of each store watches for the first event and lists which standard events it has seen, so a half-finished install is obvious. Live view shows events landing as they happen. An event missing a required property is rejected and named in the response.
1.Add the snippet to your header
Appearance → Theme File Editor → header.php, or any "insert headers" plugin.
<script>window.riq=window.riq||function(){(riq.q=riq.q||[]).push(arguments)};</script>
<script async src="https://cdn.recartiq.com/r.js" data-key="pk_..."></script>Both lines, in that order. The first is a queue: the snippet loads asynchronously so it does not slow your store down, and without the queue a call made before it finishes throws and the event is lost. Your key is on the Install page of the store and in Settings → API keys.
That alone gives you page views, sessions, device, country and UTM attribution, plus out-of-stock views, empty searches, size-guide opens and coupon or checkout errors, all detected from the WooCommerce theme. Product views are the exception: the theme exposes a price but no currency and the event needs both, so sendview_product yourself in step 2.
2.Send the commerce events
All of these work, including view_product on the product template, which Woo does not detect for you yet.
riq("track", "view_product", { product_id: "SKU-1", name: "Trail Jacket", price: 129, currency: "USD" });
riq("track", "add_to_cart", { product_id: "SKU-1", price: 129, currency: "USD", quantity: 1, cart_value: 129 });
riq("track", "begin_checkout", { cart_value: 129, currency: "USD" });
riq("identify", "customer-123", { email: "c@example.com" });No JavaScript to hand? Annotate the button instead:
<button data-riq-event="add_to_cart" data-riq-product-id="SKU-1" data-riq-price="129" data-riq-currency="USD">Add to cart</button>
3.Send the purchase from the thank-you hook
Your own site runs the checkout, so a theme hook can send the order directly.
add_action('woocommerce_thankyou', function ($order_id) {
$o = wc_get_order($order_id);
?><script>
riq("track", "purchase", {
order_id: "<?php echo esc_js($o->get_order_number()); ?>",
revenue: <?php echo esc_js($o->get_total()); ?>,
currency: "<?php echo esc_js($o->get_currency()); ?>"
});
</script><?php
});4.Check it arrived
The Install page of each store watches for the first event and lists which standard events it has seen, so a half-finished install is obvious. Live view shows events landing as they happen. An event missing a required property is rejected and named in the response.
Sending from a backend instead? The HTTP API below takes the same events, against recartiq.com.
Friction events
These turn "checkout conversion fell" into a reason. Send the ones your theme does not expose:
riq("track", "shipping_cost_viewed", { amount: 8.9, currency: "USD", cart_value: 129 });
riq("track", "coupon_rejected", { code: "FALL10", reason: "expired" });
riq("track", "payment_failed", { method: "card", reason: "declined" });Event reference
| Event | Required properties |
|---|---|
page_view | none |
view_product | product_id, price |
add_to_cart | product_id, price |
remove_from_cart | product_id, price |
begin_checkout | cart_value |
purchase | order_id, revenue |
refund | order_id, revenue |
identify | none (call riq identify) |
checkout_error | none |
coupon_rejected | code |
payment_failed | none |
out_of_stock_view | product_id |
variant_unavailable | product_id, variant |
search_no_results | query |
shipping_cost_viewed | amount |
size_guide_opened | product_id |
Every amount is tracked in the store's own currency, set in Settings. currency is optional; if you send a different code the amount is counted at face value and the code you sent is kept as original_currency. remove_from_cart is stored and available in the Events explorer and custom funnels, but no built-in report uses it. quantity defaults to 1 where it applies. Custom events: any name matching ^[a-z][a-z0-9_]{0,63}$ with free-form properties. Keys named password, card_number, cardnumber, cvv, cvc or ssn are dropped on arrival and never stored.
Server-side (HTTP API)
POST https://recartiq.com/api/v1/track
Authorization: Bearer pk_...
Content-Type: application/json
{ "batch": [
{ "event": "purchase", "insert_id": "<uuid>", "timestamp": "2026-09-16T10:12:03Z",
"distinct_id": "customer-123",
"properties": { "order_id": "1001", "revenue": 137.9, "currency": "USD" } }
] }Up to 100 events and 512 KB per request. The key also works as ?k= or an X-RecartIQ-Key header. Always send insert_id and timestamp so retries deduplicate. Timestamps more than 7 days old, or more than 5 minutes ahead, are clamped.
Privacy
The snippet respects Do Not Track and Global Privacy Control by default, sets first-party cookies only, and never sends IP addresses or raw user agents to storage. Delete a customer's data with DELETE /api/v1/persons/:distinct_id or from Settings → Data.