This page is for whoever owns your site's code. Send them this link. It covers the tracking snippet, consent mode, running several domains, per-order revenue, identified traffic, and visitor properties.
The snippet below is the shape, with a placeholder where your site key goes, so don't paste it as-is. Shopiens hands you the real one when you add your site at app.shopiens.ai/start, and it stays available in the app under Settings.
<script>
window.shopiens=window.shopiens||function(){(window.shopiens.q=window.shopiens.q||[]).push(arguments)};
(function(){var s=document.createElement("script");s.src="https://app.shopiens.ai/script.js";s.setAttribute("data-shopiens","YOUR-SITE-KEY");(document.head||document.documentElement).appendChild(s);})();
</script>
Put it in the <head> of every page: directly in your templates, or as a custom-HTML tag in your tag manager (Google Tag Manager, etc.) fired on all pages. The snippet is deliberately self-injecting so tag managers can't strip its site key; paste it whole, don't convert it to a bare external script.
Installed the snippet before? An earlier snippet loads from app.evarist.ai and says evarist where this one says shopiens. It keeps tracking, and so do the evarist(…) calls you wrote, which work like the shopiens(…) calls on this page. Until you replace it, write evarist(…) where this page says shopiens(…). When you replace it, rename your calls at the same time: before the snippet finishes loading, a page knows only the name its own snippet declares, and a call under the other name stops the code around it.
It weighs about 14 KB compressed, loads asynchronously after your page, never blocks rendering, and fails silently if it can't load, so your site is unaffected either way.
Separately from this snippet, Shopiens also fetches your pages server-side, to build the site model and to verify on-page tags. If your WAF or CDN needs an allowlist rule for that crawler, see the ShopiensBot page.
By default the snippet sets its first-party analytics cookies as soon as it loads. If your compliance posture requires no identifying cookie before consent, switch on consent mode: Shopiens still counts every visitor from their first page, but keeps its identifiers in per-tab session storage, with no identifying cookie, until you signal that the visitor consented. Add one line inside your snippet, before the loader:
<script>
window.shopiens=window.shopiens||function(){(window.shopiens.q=window.shopiens.q||[]).push(arguments)};
shopiens('consent','pending'); // ← the one added line
/* …the rest of your Shopiens snippet, unchanged… */
</script>
Then, from your cookie banner's callbacks, signal the visitor's choice:
shopiens('consent', { analytics: true }); // consented: switch to cookies, keep the same visit
shopiens('consent', { analytics: false }); // declined or withdrawn: remove Shopiens' cookies
Consent is remembered: once granted, later pages start in cookie mode on their own, and shopiens('consent','pending') deliberately leaves the _shopiens cookie of an earlier grant in place. "No identifying cookie" describes a visitor who hasn't consented yet, not every page load. On a Shopify store whose cookie banner uses Shopify's Customer Privacy API (Shopify's own banner does), skip the recipes below: the Shopiens app embed checks that signal before it loads the tracker, and if you track the store with the snippet instead, consent mode follows the same signal. With a banner that doesn't use that API, the app embed can't see the visitor's choice and loads the tracker at page load: switch to a banner that does, or track the store with the snippet and one of the recipes below. A headless storefront carries that signal only if it loads Shopify's customer-privacy runtime; otherwise use the recipes like any other site.
Recipes for popular consent tools (category names vary per setup, so map your analytics category, and check your tool's current docs):
// Cookiebot
window.addEventListener('CookiebotOnAccept', function () {
if (Cookiebot.consent.statistics) shopiens('consent', { analytics: true });
});
window.addEventListener('CookiebotOnDecline', function () {
shopiens('consent', { analytics: false });
});
// OneTrust (C0002 is the default "Performance" group; use your own group id)
function OptanonWrapper() {
var on = (',' + (window.OnetrustActiveGroups || '') + ',').indexOf(',C0002,') !== -1;
shopiens('consent', { analytics: on });
}
// Axeptio (use the vendor/step names from your project)
window._axcb = window._axcb || [];
window._axcb.push(function (axeptio) {
axeptio.on('cookies:complete', function (choices) {
shopiens('consent', { analytics: !!choices.analytics });
});
});
// Any other CMP: call the two lines above from its accept / decline callbacks.
Trade-off to know: until a visitor consents, their identity is per-tab and per-visit: pageviews and sessions stay accurate, but a returning non-consented visitor counts as new. That's the conservative behavior by design.
Run the same brand on several domains, your .com and .fr, or a separate checkout domain? Install the same snippet on every domain, then add the other domain in the app, under Settings, Domains. From then on a visitor who crosses between them is recognised as one person, and their journey reads as one journey.
The same panel lists a site's domains and removes one at any time.
When the revenue amount varies per order and your checkout is your own code, fire the conversion yourself:
shopiens('goal', 'purchase', { value: 49.90 });
The value is in major units (49.90 = €49.90). This fires the same conversion event as a no-code goal and respects the same per-session dedup.
Two input rules: the goal name is lowercased, then must be 1 to 29 characters of letters, digits, _ or -, with no spaces (an invalid name is dropped with a console warning). And value must be a number, not a string: { value: 49.90 }, never { value: "49.90" }, because a string is ignored and the conversion fires without revenue.
For fixed-value outcomes (a signup worth €49), skip the code and ask Shopiens to create the goal with a value instead: Track what matters.
Shopiens can see that someone opened a product page. Whether it can tell you which product depends on how your store is built.
On Shopify, the id is already collected. The native app's web pixel sends the product id with each product view, so the data is there with nothing to wire. Asking about individual products on top of it is still being finished, so if that is what you are after, tell us and we will let you know the moment it lands. See Shopiens on Shopify.
On your own storefront code, not yet. The tracker's track call takes an event name and nothing else, so there is no supported way to attach a product id to a view today. If that is what you need, tell us: it is on our list, and knowing it is blocking you moves it up.
Optionally, Shopiens can connect the email a visitor submits on one of your forms to their on-site behaviour, so "who asked for a demo this week, and what did they look at first?" becomes answerable. It's off by default: we switch it on for a site when you ask, and even then it captures nothing until the visitor has consented. Captured email addresses are deleted after 12 months.
Turn it on: ask us to switch it on for your site. It needs Shopiens' own tracker on the page.
The consent gate. Captured emails are only sent after an explicit consent signal reaches the tracker. If you use consent mode, the shopiens('consent', { analytics: true }) call you already wire is that signal; if you don't, wire that same one call from your cookie banner's accept path. On a Shopify store tracked with the snippet, switch on consent mode and the store's banner drives it.
What gets captured. Emails submitted in Wisepops popups and embeds are picked up automatically. For any other form, call the tracker from your own submit handler:
shopiens('identify', { email: 'visitor@example.com' });
From then on, ask about identified visitors in the app, or in your assistant. Capture can be switched off the same way, at any time. A page a visitor already has open keeps capturing until it is reloaded or closed; new page loads see the change within about a minute.
Attach a fact you know about a visitor, such as is_employee, plan or est_important, so you can ask Shopiens to segment or exclude by it. It's declared-only: the tracker only ever sends a property name you declared first, up to 20 per site.
Declare it: ask your assistant ("declare a property called is_employee"), or use the Properties panel in the app under Settings. Pick a type (boolean, string, or number) and where the value comes from: your own code, or source data_layer (a GTM dataLayer path).
Set it from your code. Call the tracker with the declared name and a matching value:
shopiens('set', { is_employee: true });
shopiens('set', { plan: 'gold', seats: 12 });
An undeclared name never leaves the browser: no error, it is silently dropped. A value that doesn't match the declared type (a string where a number was declared, say) is dropped the same way. String values are capped at 256 characters.
Or map it from your dataLayer. Declare the property with source data_layer and a dot-path (e.g. user.vip), and the tracker reads it directly, no extra call needed:
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ user: { vip: true } });
// declared with source "data_layer", path "user.vip": the tracker picks it up
// from the existing dataLayer on load, and from every later push, automatically
Consent-gated, exactly like identified traffic: nothing ships before shopiens('consent', { analytics: true }). A value set before consent is queued, not dropped: it sends on the first event after the visitor consents.
Property names that look like personal data (email, phone, …) are rejected at declare time, because properties are for segmentation, not identity; see identified traffic to capture an email instead.