Tracking script
The tracking script is a connector's third channel. It creates the visitor, records page views, feeds carts and triggers scenarios and widgets in the browser.
Without it, abandoned-cart recovery and widgets do not work, carts cannot be created through the data API.
Tracking integrates with the spm.js loader: a four-line tag pasted once, then commands called as your site's events happen. The loader takes care of everything else, visitor identity, cart sends, consent.
Step 1: Paste the tag
Once per document load (not per route), ideally in the <head>:
<script>
window.spm = window.spm || function () { (spm.q = spm.q || []).push(arguments); };
spm('consent', true); // ⚠ replace with your cookie banner's answer, step 2
spm('init', 'SPM123456'); // your shop identifier (Settings → API access)
</script>
<script async src="https://v2.app-spm.com/api/assets/js/spm.js"></script>That is all that is mandatory. spm() exists before the file even loads: calls stack up in a queue and are replayed when the script arrives, load order does not matter, you can call spm('…') from any component, theme or tag manager.
First-party tracking domain
If your shop has a verified tracking domain (e.g. t.my-shop.com), simply change the host in the tag's src. The loader reads its own origin: all its calls follow automatically.
Step 2: Wire consent (your CMP)
A CMP (Consent Management Platform) is your cookie banner, Axeptio, Didomi, OneTrust, Cookiebot, tarteaucitron… It is what collects and remembers the visitor's choice, category by category.
The spm('consent', true) line in the tag is a placeholder to replace: pass it your banner's actual answer for the category tracking depends on. The loader is strict opt-in:
- until
spm('consent', true)is received, no request leaves: everything is queued; - on
spm('consent', false)(refusal or withdrawal), the loader stops all sends and purges the local visitor identifier.
Example with Axeptio:
window._axcb = window._axcb || [];
window._axcb.push(function (axeptio) {
axeptio.on('cookies:complete', function (choices) {
spm('consent', choices.shopimind === true); // the vendor name configured in your CMP
});
});With tarteaucitron:
tarteaucitron.user.shopimindConsent = function (accepted) {
spm('consent', accepted);
};The principle is the same whatever the CMP: it decides, the loader obeys. A site with no cookie banner (internal tool, closed B2B) can leave true hard-coded.
Excluding specific pages from tracking
Independently of consent, specific pages can be excluded from tracking (checkout funnel, account area, legal pages…). This is a shop-side configuration at ShopiMind, ask us to enable it and send us the URLs to exclude.
Step 3: Call the commands on your events
| Command | When to call it | Effect |
|---|---|---|
spm('init', ident, opts?) | Once, in the tag | Starts tracking: creates or finds the visitor, loads the engine, scenarios and widgets. opts.trackingUrl overrides the base if needed. |
spm('consent', bool) | On every CMP decision | Opens (true) or closes (false) all emissions. |
spm('identify', { id_customer }) | When the session is authenticated, before init if it is already known at load time | Links the visitor to their customer account, your identifier, the same as customer_id in the API. Called before init, the link is immediate; after, it applies on the next cart send. |
spm('logout') | On sign-out | Stops sending the customer identifier. Does not touch the visitor. |
spm('page', ctx) | Before init, when the context is known | Page context: id_product, id_category, id_manufacturer, id_combination, id_cart. Feeds scenario targeting. |
spm('cart', cart, { lang }?) | On every cart mutation: add, remove, quantity, promo code, emptying included | Sends the cart (built-in debounce). The cart shape is described below. |
spm('order', { id_cart, id_order }) | Before init, on the confirmation page | Closes the cart and attributes the conversion. |
// On sign-in / sign-out
spm('identify', { id_customer: '4212' });
spm('logout');
// On a product page, before init
spm('page', { id_product: 'P-4471', id_combination: 'V-2' });
// On every cart mutation
spm('cart', {
id_customer: '4212', id_cart: 'A7C31F',
date_add: '2026-08-04T10:12:07.000000+02:00',
date_upd: '2026-08-04T10:40:55.000000+02:00',
amount: '129.90', amount_without_tax: '108.25',
tax_rate: '1', currency: 'EUR',
voucher_used: [], voucher_amount: '0',
products: [
{ id_product: 'P-4471', id_combination: 'V-2', id_manufacturer: 'M-8',
qty: '2', price: '64.95', price_discount: '58.45',
price_without_tax: '54.12' }
],
}, { lang: 'en' });
// On the order confirmation page, before init
spm('order', { id_cart: 'A7C31F', id_order: 'O-9912' });The cart object shape
| Field | Type | Role |
|---|---|---|
id_cart | string | Cart identifier in your system. Keep it stable across pages: it is the tracking key. |
id_customer | string | Your customer identifier (the same as customer_id in the API). "" when the visitor is anonymous. |
date_add / date_upd | ISO 8601 | Cart creation and last modification, with microseconds and the real offset: 2026-08-04T10:12:07.000000+02:00. |
amount | string | Total, tax included. |
amount_without_tax | string | Total, tax excluded. |
tax_rate | string | Currency conversion rate (most often "1"). |
currency | ISO 4217 | Cart currency. |
voucher_used | array | Applied promo codes (array of strings, [] if none). |
voucher_amount | string | Total discount amount ("0" if none). |
products | array | One entry per cart line, see below. |
Each products entry:
| Field | Type | Role |
|---|---|---|
id_product | string | Product identifier (the same as product_id in the API). |
id_combination | string | Selected variation ("" if none). |
id_manufacturer | string | Manufacturer ("" if none). |
qty | string | Quantity. |
price | string | Catalog unit price, tax included. |
price_discount | string | Actually paid unit price, tax included (discounts applied). |
price_without_tax | string | Unit price, tax excluded. |
Two rules to remember:
- every identifier and amount is a string, never a number;
- send emptied carts too (
products: [],amount: "0"): that is what cancels a reminder on a cart that was abandoned then emptied.
Verify your integration
| Check | Expected |
|---|---|
| Network tab, before consent | No call to the tracking domain |
| Network tab, after consent | spm.js loaded, followed by tracking starting on the same domain |
| After adding to cart | A request leaves for the tracking domain and answers { "success": true } |
First-party tracking domain
By default, tracking is served from our domain. On a headless front end (your site on www.my-shop.com, tracking on a third-party domain), browser protections kick in: Safari, in particular, blocks third-party cookies, and part of the carts can be lost silently.
The answer is a tracking domain under your own name: declare, say, t.my-shop.com in My domains, create the indicated CNAME record, and we activate the domain. From then on:
- visitor identity becomes first-party → no more Safari or Firefox blocking;
- every tracking call goes to your own domain;
- your CMP classifies tracking as first-party, simplifying its categorisation;
- your email links come from the same domain, a deliverability bonus along the way.
Once the domain is active, simply change the host in the tag's src: the loader follows.
SPA navigation
On a single-page application (React, Vue, Nuxt…), here is what works continuously and what does not:
| Status | |
|---|---|
| Visitor identity, session | Works: established at load, persistent |
Cart, through spm('cart', …) | Works: call it on every mutation, whatever the route |
| Widgets and scenarios loaded at startup | Work |
Page-context targeting (spm('page')) on virtual navigation | No: context is only read on a full document load |
In practice: keep calling spm('cart', …) on every mutation, that is what carries cart recovery. Scenarios targeted at a specific URL or product page re-evaluate on the next full document load.
If your shop is a full SPA and per-page targeting is central for you, contact us: we are working on dedicated support.
Next
→ Reference: terminology, target scope, troubleshooting.