Tracking Shopify Purchases with a Custom Pixel
Send completed Shopify purchases directly to Pertento while preserving the experiment and variant assignments needed for attribution.
5 min read
Send completed Shopify purchases directly to Pertento while preserving the experiment and variant assignments needed for attribution.
This setup uses Shopify Customer events and Pertento’s Tracking API. No additional reporting server is required.
Before you start
You need:
- Pertento installed on the storefront.
- Your Pertento website ID.
- A running experiment with registered visitor exposures.
- Pertento native tracking selected for experiment evaluation, rather than an external analytics integration. See Reporting Conversions and Events.
This example requires the visitor’s Pertento storage to be accessible at checkout. Verify that condition in your store before relying on the integration.
How experiment attribution works
Pertento stores experiment assignments in PERTENTO_EXP_VAR:
{ "5961": "14491" }The keys are experiment IDs; the values are variant IDs. The pixel also checks the corresponding PERTENTO-VISITED-<experimentId> flag before including an assignment. See How Pertento uses local storage.
The pixel sends eligible assignments as experimentVariantMap, alongside the purchase amount and currency. This mapping connects the purchase to the experiment variants. See Posting Conversion Events via the Tracking API.
Add the custom pixel
In Shopify admin:
- 1Open Settings → Customer events.
- 2Add a custom pixel named Pertento.
- 3Paste the code below.
- 4Replace
YOUR_WEBSITE_IDwith your numeric Pertento website ID. - 5Review the pixel’s customer privacy settings.
- 6Save and connect the pixel.
const WEBSITE_ID = 'YOUR_WEBSITE_ID';
const TRACKING_URL = 'https://tracking.pertento.ai/events';
const EVENT_NAME = 'purchase';
async function variantMap() {
const raw = await browser.localStorage.getItem('PERTENTO_EXP_VAR');
const assignments = JSON.parse(raw || '{}');
if (
!assignments ||
typeof assignments !== 'object' ||
Array.isArray(assignments)
) {
throw new Error('Invalid PERTENTO_EXP_VAR format');
}
const map = {};
for (const [expId, value] of Object.entries(assignments)) {
const variantId = String(value);
const visited = await browser.localStorage.getItem(
'PERTENTO-VISITED-' + expId
);if (/^\d+$/.test(variantId) && visited) {
map[expId] = variantId;
}
}
return map;
}
analytics.subscribe('checkout_completed', async (event) => {
try {
if (!/^\d+$/.test(WEBSITE_ID)) {
throw new Error('Set your numeric Pertento website ID');
}
const checkout = event.data.checkout;
const orderKey = checkout.order?.id || checkout.token;
if (!orderKey) return;
const marker = 'PERTENTO_REPORTED_' + orderKey;
const legacyMarker = checkout.token
? 'PERTENTO_REPORTED_' + checkout.token
: null;
if (await browser.localStorage.getItem(marker)) return;
if (
legacyMarker &&
legacyMarker !== marker &&
(await browser.localStorage.getItem(legacyMarker))
) {
return;
}
const map = await variantMap();
if (!Object.keys(map).length) return;
const total = checkout.totalPrice;
const currency = total?.currencyCode || checkout.currencyCode;
const amount = total?.amount;
if (
!currency ||
amount === null ||
amount === undefined ||
String(amount).trim() === '' ||
!Number.isFinite(Number(amount))
) {
return;
}
const body = JSON.stringify({
dataPayload: [
[
EVENT_NAME,
{
currency,
revenue: amount,
},
],
],
experimentVariantMap: map,
isPertento: true,
});
// Records a sending attempt, not confirmed delivery.
await browser.localStorage.setItem(marker, '1');
await fetch(
TRACKING_URL + '?websiteId=' + encodeURIComponent(WEBSITE_ID),
{
method: 'POST',
keepalive: true,
headers: {
'Content-Type': 'text/plain',
},
body,
}
);
} catch {
// No automatic retry: delivery may be unknown.
}
});The example sends Shopify’s totalPrice.amount as revenue. It does not send tax or shipping separately, avoiding an additional charge on top of the amount already supplied.
Does Pertento need to load before the purchase event?
The Pertento script does not need to run on the checkout page if the required assignments and visited flags are already accessible. The pixel reads storage and sends directly to the API; it does not call pertentoReportConversion.
However, the example skips the purchase if no eligible mapping is available when the callback reads storage. This can happen when:
- Pertento has not yet recorded the visitor’s assignment.
- The visitor was never enrolled in an experiment.
- Checkout uses a different origin from the storefront.
- Storage access fails.
Shopify’s browser.localStorage API accesses storage through the top frame. It does not merge storage across origins. Shopify browser API, localStorage scope.
A short wait can help only when an existing assignment is about to become available on that page. It cannot recover inaccessible storefront storage. Do not create a new assignment after purchase merely to attribute the transaction.
For stores where storage is unavailable at checkout, use a separate integration that captures the mapping after exposure and transfers it through cart attributes before checkout. Checkout events expose those attributes. This transfer is not implemented by the example above. Shopify Cart API, checkout event fields.
Test the integration
Use Shopify’s Test button to open Pixel Helper, provide the consent required by the pixel configuration, and complete a new test purchase. After editing the pixel, close the previous test tab and start a new session. Testing custom pixels.
Shopify normally emits checkout_completed once per checkout. With post-purchase upsells, it fires on the first upsell offer page. Reloading the thank-you page is not a reliable way to repeat the test. Event behavior.
Verify the result in Pertento, with the appropriate objective and revenue currency selected. An HTTP success response alone does not confirm that the event was stored. Tracking API reference.
Delivery and duplicate protection
The local marker suppresses later attempts from the same browser storage. It is written after validation but before sending, so a failed request may leave a marker without a recorded conversion.
This is best-effort duplicate protection, not guaranteed exactly-once delivery. The example makes no automatic retries and cannot recover purchases missed because the browser closes or the pixel never runs.
While troubleshooting, inspect the browser’s Network panel. To expose exceptions temporarily, replace the final catch block with:
} catch (error) { console.error('[pertento] purchase reporting failed:', error); }Avoid running another integration that reports the same purchases to Pertento.