Skip to content
  • There are no suggestions because the search field is empty.

Shopify App: Custom Web Pixel for Granular Subscription Goal Triggering

Custom Shopify Web Pixel for Tracking Convert Goals by Subscription Plans

IN THIS ARTICLE YOU WILL:

Overview

This guide helps you create a custom web pixel to trigger specific Convert goals based on exact Shopify subscription plan types. This solution is ideal when you need more granular control than the standard subscription/non-subscription binary approach provided by our official Shopify Web Pixel.

Use Cases

Use this custom web pixel when you need to:
 
  • Trigger different goals for specific subscription plan durations (e.g., 1-month vs 3-month plans)
  • Track conversions for orders without any subscriptions (regular products + one-time purchases)
  • Track conversions for specific selling plan IDs
  • Complement your existing Convert Shopify Web Pixel with additional goal triggering logic

Prerequisites

  • Convert account with active project
  • Shopify store with a subscription app installed (e.g., Shopify Subscriptions)
  • Subscription products configured with selling plans
  • Basic knowledge of Shopify's Custom Web Pixels
  • Access to your Shopify Admin panel

Step 1: Gather Required Information

Before creating the custom web pixel, collect the following:
 

Convert Information

  • Account ID: Found in your Convert project settings
  • Project ID: Found in your Convert project settings
  • Goal IDs: The specific Convert goal IDs you want to trigger
 

Shopify Information

  • Selling Plan IDs: The Shopify selling plan IDs for your subscription products
 
⚠️ Important: This custom web pixel requires subscription products with selling plans. Shopify does not include selling plans by default - you need a subscription app.
 

Subscription App Requirements:

This custom web pixel works with subscription apps that create Shopify selling plans, such as:
 
  • Shopify Subscriptions (Official Shopify app) - Recommended
  • ReCharge Subscriptions
  • Bold Subscriptions
  • Other subscription apps that use Shopify's native selling plans
 

How to Find Selling Plan IDs with Shopify Subscriptions App:

Prerequisites:
 
  1. Install the Shopify Subscriptions app from the Shopify App Store
  2. Create subscription plans for your products
  3. Ensure products have subscription options enabled
 

Finding the Selling Plan ID:

 
  1. In your Shopify Admin, go to AppsSubscriptions
  2. Click on Subscription plans or Plans
  3. Click on the specific subscription plan you want to track
  4. Look at the URL in your browser address bar:
    • URL example: https://admin.shopify.com/store/your-store/apps/subscriptions-remix/app/plans/1862795494
    • The selling plan ID is the number at the end: 1862795494
  5. The full selling plan ID format for the code will be: gid://shopify/SellingPlan/1862795494
  6. Repeat this process for each subscription plan you want to track
 

Alternative method using browser developer tools:

 
  1. Go to your storefront and view a product with subscription options
  2. Open browser developer tools (F12)
  3. Go to the Network tab and reload the page
  4. Look for GraphQL or API requests containing selling plan data
  5. The selling plan IDs will be in the format gid://shopify/SellingPlan/XXXXXXXX
 
For other subscription apps: If you're using a different subscription app, the method may vary. Contact your subscription app provider for guidance on finding selling plan IDs, or use the browser developer tools method above.

Step 2: Create the Custom Web Pixel

  1. In your Shopify Admin, go to SettingsCustomer Events
  2. Click Add Custom Pixel
  3. Enter a name (e.g., "Convert Subscription Goals")
  4. Copy and paste the following code:
 
📖 Need help with custom pixels? For detailed instructions on managing custom pixels in Shopify, see the official Shopify documentation.
 
const convert = new Convert({
accountId: "YOUR_CONVERT_ACCOUNT_ID", // Replace with your Convert Account ID
projectId: "YOUR_CONVERT_PROJECT_ID", // Replace with your Convert Project ID
});

const convertGoalsMapping = {
noSubscription: {
goalId: "YOUR_NO_SUBSCRIPTION_GOAL_ID", // Replace with your Convert Goal ID
// Note: Triggers for any order without subscription items (regular products OR one-time purchases)
},
oneMonthSubscription: {
goalId: "YOUR_ONE_MONTH_GOAL_ID", // Replace with your Convert Goal ID
sellingPlanId: "YOUR_ONE_MONTH_SELLING_PLAN_ID", // Replace with your Shopify Selling Plan ID
},
threeMonthSubscription: {
goalId: "YOUR_THREE_MONTH_GOAL_ID", // Replace with your Convert Goal ID
sellingPlanId: "YOUR_THREE_MONTH_SELLING_PLAN_ID", // Replace with your Shopify Selling Plan ID
},
};

analytics.subscribe("checkout_completed", async (event) => {
console.log('Custom Web Pixel: Event fired "checkout_completed"...');
console.debug("Custom Web Pixel: Event Data:", event);

await convert.getBrowserData();
// see if cart attributes already present (might be set at external domain using cart permalinks)
convert.updateBrowserData(event);

// return if there's no visitor data
if (!convert.visitorId) {
console.debug("Custom Web Pixel: No visitor data found!");
return;
}

let goalId;
const { lineItems = [] } = event?.data?.checkout || {};

// Check for orders with no subscription items
// This includes: regular products AND one-time purchases of subscription products
if (
!convert.hasSubscriptionPlan(lineItems)
) {
console.log(
`Custom Web Pixel: No subscription plan detected for goal "${convertGoalsMapping.noSubscription.goalId}"`
);
goalId = convertGoalsMapping.noSubscription.goalId;
}

// Check for specific subscription plans
if (
convert.matchSubscriptionPlan(
lineItems,
convertGoalsMapping.oneMonthSubscription
)
) {
console.log(
`Custom Web Pixel: One month subscription plan matched for goal "${convertGoalsMapping.oneMonthSubscription.goalId}"`
);
goalId = convertGoalsMapping.oneMonthSubscription.goalId;
}
if (
convert.matchSubscriptionPlan(
lineItems,
convertGoalsMapping.threeMonthSubscription
)
) {
console.log(
`Custom Web Pixel: Three month subscription plan matched for goal "${convertGoalsMapping.threeMonthSubscription.goalId}"`
);
goalId = convertGoalsMapping.threeMonthSubscription.goalId;
}

if (!goalId) {
console.log("Custom Web Pixel: No subscription plan matched for goal");
return;
}

// see if there is any test for which this goal was not triggered
const bucketingData = {};
let sendConversionEvent = false;
for (const experienceId in convert.experiencesData) {
const experience = convert.experiencesData[experienceId];
// return if this is a 1:1 experience, nothing to save there;
if (
convert.shouldTrackEvent(experienceId) && // ignore experiences that are not active anymore or if this is a 1:1 experience
experience?.[Convert.ExperienceData.VARIATION] === "1" // skip experiences for which user was excluded
) {
console.log(
`Custom Web Pixel: Skip triggering goal ${goalId} for experience ${experienceId}!`
);
continue;
}
if (!experience?.[Convert.ExperienceData.GOALS]?.[goalId]) {
experience[Convert.ExperienceData.GOALS][goalId] = 1;
sendConversionEvent = true;
} else {
console.log(
`Custom Web Pixel: Goal ${goalId} already triggered for experience ${experienceId}...ignoring`
);
}
if (experience?.[Convert.ExperienceData.VARIATION])
bucketingData[String(experienceId)] = String(
experience[Convert.ExperienceData.VARIATION]
);
}
// attempt to update visitor cookie anyway with triggered goals
convert.setVisitorCookie(convert.experiencesData);

// return if we do not have new goals triggered
if (!sendConversionEvent) {
console.log(
"Custom Web Pixel: We have no goals triggered now, skip tracking conversion event(s)..."
);
return;
}

// return if there's no bucketing data
if (!Object.keys(bucketingData).length) {
console.log(
`Custom Web Pixel: Goal #${goalId} triggered now with no bucketing data, skip tracking conversion event(s)...`
);
return;
}

// send conversion event only
convert.trackConversion({
eventType: "conversion",
data: { goalId, bucketingData },
});
});

class Convert {
static VISITOR_COOKIE_TTL = 15768000; // 1 year tracking duration

static ExperienceData = {
GOALS: 'g',
VARIATION: 'v'
};

static StorageKey = {
DATA: 'convert.com'
};

static CookieKey = {
DATA: '_conv_d',
SEGMENTS: '_conv_g',
VISITOR: '_conv_v',
SESSION: '_conv_s',
ATTRIBUTES: 'convert_attributes'
};

visitorId = "";
experiencesData = {};

#_accountId;
#_projectId;
#_experiences;
#_segments = {};
#_sessionCookie = {};
#_visitorCookie = {};

#_sessionCookieCache = "";
#_visitorCookieCache = "";
#_domain = "";
#_isManualSetup = false;

constructor({ accountId, projectId }) {
this.#_accountId = accountId;
this.#_projectId = projectId;
}

async getBrowserData() {
try {
await this.#getBrowserCookies();
const { visitorId, experiencesData } = this.#decodeCookieValueJson();
if (visitorId) {
const segments = await this.#getSegmentsCookie();
const data = (await this.#getDataCookie()) || {};
this.#setBrowserData({
...data,
visitorId,
experiencesData,
segments,
});
return;
}

const storageData = await browser.localStorage.getItem(Convert.StorageKey.DATA);
if (storageData) {
const { shopifyData } = JSON.parse(storageData);
if (shopifyData) {
this.#setBrowserData(shopifyData);
return;
}
}

const cookieAttributes = await browser.cookie.get(Convert.CookieKey.ATTRIBUTES);
if (cookieAttributes) {
console.log("Custom Web Pixel: Manual integration detected!");
this.#_isManualSetup = true;
const {
vid: visitorId,
exps: experiences = [],
vars: variations = [],
defaultSegments: segments = {},
} = JSON.parse(this.#decodeURIComponentSafe(cookieAttributes));
const experiencesData = {};
for (let i = 0; i < experiences.length; i++) {
const experienceId = experiences[i];
const variationId = variations[i];
if (!experienceId || !variationId) continue;
experiencesData[experienceId] = {
[Convert.ExperienceData.VARIATION]: String(variationId),
[Convert.ExperienceData.GOALS]: {},
};
}
this.#setBrowserData({ visitorId, experiencesData, segments });
}
} catch (error) {
console.warn("Custom Web Pixel: Unable to find bucketing data!");
console.debug(error);
}
}

#setBrowserData(data) {
const {
visitorId = "",
experiencesData = {},
segments = {},
experiences,
domain = "",
} = data;
if (visitorId) this.visitorId = visitorId;
if (this.#objectNotEmpty(experiencesData))
this.experiencesData = this.#objectDeepMerge(
this.experiencesData,
experiencesData
);
if (this.#objectNotEmpty(segments)) this.#_segments = segments;
if (Array.isArray(experiences)) this.#_experiences = experiences;
if (domain) this.#_domain = domain;
}

#objectNotEmpty(object) {
return (
typeof object === "object" &&
object !== null &&
Object.keys(object).length > 0
);
}

#objectDeepMerge(...objects) {
const isObject = (obj) => obj && typeof obj === "object";

return objects.reduce((prev, obj) => {
Object.keys(obj).forEach((key) => {
const pVal = prev[key];
const oVal = obj[key];

if (Array.isArray(pVal) && Array.isArray(oVal)) {
prev[key] = [...new Set([...oVal, ...pVal])];
} else if (isObject(pVal) && isObject(oVal)) {
prev[key] = this.#objectDeepMerge(pVal, oVal);
} else {
prev[key] = oVal;
}
});

return prev;
}, {});
}

#decodeURIComponentSafe(value) {
if (!value) return value;
try {
return decodeURIComponent(value.replace(/%(?![0-9a-fA-F]{2})/g, "%25"));
} catch (e) {
return decodeURIComponent(value.replace(/%[0-9a-fA-F]{2}/g, "%20"));
}
}

async #getDataCookie() {
const cookie = await browser.cookie.get(Convert.CookieKey.DATA);
if (!cookie) return;
try {
return JSON.parse(this.#decodeURIComponentSafe(cookie));
} catch (error) {
console.warn("Custom Web Pixel: error decoding data cookie");
console.debug(error);
return {};
}
}

async #getSegmentsCookie() {
const cookie = await browser.cookie.get(Convert.CookieKey.SEGMENTS);
if (!cookie) return;
try {
return JSON.parse(this.#decodeURIComponentSafe(cookie));
} catch (error) {
console.warn("Custom Web Pixel: error decoding segments cookie");
console.debug(error);
return {};
}
}

async #getBrowserCookies() {
const sessionCookie = await browser.cookie.get(Convert.CookieKey.SESSION);
if (!sessionCookie) return;
this.#_sessionCookieCache = this.#decodeURIComponentSafe(sessionCookie);
if (!this.#_sessionCookieCache) return;
const sessionData = parse(this.#_sessionCookieCache);
if (!sessionData?.sh) return;
this.#_sessionCookie = {
...this.#_sessionCookie,
...sessionData,
};

const visitorCookie = await browser.cookie.get(Convert.CookieKey.VISITOR);
if (!visitorCookie) return;
this.#_visitorCookieCache = this.#decodeURIComponentSafe(visitorCookie);
if (!this.#_visitorCookieCache) return;
const visitorData = parse(this.#_visitorCookieCache);
if (!visitorData?.vi) return;
visitorData.vi = visitorData.vi === "1" ? sessionData.sh : visitorData.vi;
this.#_visitorCookie = {
...this.#_visitorCookie,
...visitorData,
};

function parse(input) {
let data = {};

let chSplit = "*";
if (input.indexOf("|") != -1) chSplit = "|";
const cookieParts = input.split(chSplit);
for (let i = 0, l = cookieParts.length; i < l; i++) {
const key_value = cookieParts[i].split(":");
if (typeof key_value[1] !== "undefined")
data[key_value[0]] = key_value[1];
else data = {};
}

return data;
}
}

updateBrowserData(event) {
try {
const { attributes = [] } = event?.data?.checkout || {};
const { __data: data } = Object.fromEntries(
attributes.map(({ key, value }) => [key, value])
);
if (!data) return false;
const { domain, experiences, visitorData } = JSON.parse(atob(data));
if (!visitorData) return false;
const { visitorId, bucketing, segments = {} } = visitorData;
if (!visitorId || !Array.isArray(bucketing)) return false;
const experiencesData = Object.fromEntries(
bucketing.map(({ experienceId, variationId, goals = [] }) => [
experienceId,
{
v: String(variationId),
g: Object.fromEntries(
goals.map(({ goalId }) => [String(goalId), 1])
),
},
])
);
this.#setBrowserData({
visitorId,
experiencesData,
segments,
experiences,
domain,
});
return true;
} catch (error) {
console.warn(
"Custom Web Pixel: Unable to find bucketing data in checkout attributes!"
);
console.debug(error);
console.debug(event);
return false;
}
}

hasSubscriptionPlan(lineItems) {
return lineItems.some((item) => {
const { id } = item?.sellingPlanAllocation?.sellingPlan || {}; // Note: assuming the official Sybscriptions App is used – https://apps.shopify.com/shopify-subscriptions
return id;
});
}

matchSubscriptionPlan(lineItems, targetGoal) {
return Object.fromEntries(
lineItems.some((item) => {
const { id } = item?.sellingPlanAllocation?.sellingPlan || {}; // Note: assuming the official Sybscriptions App is used – https://apps.shopify.com/shopify-subscriptions
if (id === targetGoal?.sellingPlanId) return true;
return false;
})
);
}

shouldTrackEvent(experienceId) {
if (!Array.isArray(this.#_experiences)) {
if (this.#_isManualSetup) {
console.log(
`Custom Web Pixel: Assume active experience ${experienceId} due to manual integration.`
);
} else {
console.log(
`Custom Web Pixel: Assume active experience ${experienceId} since verify data is not provided.`
);
}
return true;
}
return this.#_experiences.includes(String(experienceId));
}

setVisitorCookie(experiences) {
if (!this.#_domain) {
if (this.#_isManualSetup) {
console.log(
"Custom Web Pixel: We cannot update visitor cookie due to manual integration."
);
} else {
console.log(
"Custom Web Pixel: We cannot update visitor cookie since verify data is not provided."
);
}
return;
}
this.#_visitorCookie.exp = encodeCookieValueJson(experiences);
const expires = new Date();
expires.setTime(Date.now() + 1000 * Convert.VISITOR_COOKIE_TTL);
browser.cookie.set(
`${Convert.CookieKey.VISITOR}=${encodeURIComponent(
encodeData(this.#_visitorCookie)
)};expires=${expires.toUTCString()};path=/;domain=${
this.#_domain
};SameSite=lax`
);

function encodeData(data = {}) {
const strparts = [];
for (const key in data) {
if (!data[key]) continue;
const part = `${key}:${String(data[key])
.replace(/:/g, "")
.replace(/\*/g, " ")
.replace(/\|/g, "-")}`;
strparts.push(part);
}
return strparts.join("*");
}

function encodeCookieValueJson(data) {
return JSON.stringify(data)
.replace(/,/g, "-")
.replace(/:/g, ".")
.replace(/"/g, "");
}
}

#decodeCookieValueJson() {
const { vi: visitorId, exp } = this.#_visitorCookie || {};
if (!visitorId || typeof exp !== "string") return {};
try {
const experiencesData = decode(exp) || {};
// force string values
for (const experienceId in experiencesData)
if (typeof experiencesData[experienceId]?.[Convert.ExperienceData.VARIATION])
experiencesData[experienceId][Convert.ExperienceData.VARIATION] = String(
experiencesData[experienceId][Convert.ExperienceData.VARIATION]
);
return {
visitorId,
experiencesData,
};
} catch (error) {
const { stack, message } = error;
if (typeof console !== "undefined" && console.warn) {
console.warn("Custom Web Pixel: error decoding visitor cookie");
console.debug(stack || message);
console.debug(
String(exp)
.replace(/-/g, ",")
.replace(/\./g, ":")
.replace(/([A-Za-z0-9]+):/g, '"$1":')
);
}
return {};
}

function decode(input) {
return JSON.parse(
String(input)
.replace(/-/g, ",")
.replace(/\./g, ":")
.replace(/([A-Za-z0-9]+):/g, '"$1":')
);
}
}

trackConversion(event) {
if (!this.visitorId || !this.#objectNotEmpty(this.#_segments)) {
console.log(
"Custom Web Pixel: We have no visitor data, skip tracking conversion event(s)..."
);
return;
}
const goalId = event?.data?.goalId;
console.log(
`Custom Web Pixel: Tracking conversion event for goal #${goalId}`
);
const payload = JSON.stringify({
accountId: this.#_accountId,
projectId: this.#_projectId,
enrichData: true,
source: "shopifya",
visitors: [
{
segments: this.#_segments,
visitorId: this.visitorId,
events: [event],
},
],
});
const endpoint = `https://${
this.#_projectId
}.metrics.convertexperiments.com/v1/track/${this.#_accountId}/${
this.#_projectId
}`;
fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": payload.length,
},
body: payload,
keepalive: true,
}).catch(({ stack, message }) => {
console.warn(
`Custom Web Pixel: error tracking conversion event for goal #${goalId}`
);
console.debug(stack || message);
});
}
}

Step 3: Configure Your Mapping

Replace the placeholder values in the convertGoalsMapping object:
 
const convertGoalsMapping = {
noSubscription: {
goalId: "123456", // Your Convert Goal ID for orders without subscriptions
// Note: Covers both regular products AND one-time purchases of subscription products
},
oneMonthSubscription: {
goalId: "123457", // Your Convert Goal ID for 1-month subscriptions
sellingPlanId: "gid://shopify/SellingPlan/1234567890", // Your Shopify Selling Plan ID
},
threeMonthSubscription: {
goalId: "123458", // Your Convert Goal ID for 3-month subscriptions
sellingPlanId: "gid://shopify/SellingPlan/1234567891", // Your Shopify Selling Plan ID
},
};

Step 4: Save and Activate the Custom Web Pixel

  1. Save the custom web pixel in Shopify Admin
  2. Connect your web pixel to your store (if prompted)
  3. Set permissions - ensure the pixel has access to customer events
  4. Activate the pixel - toggle it to "Active" status
 
For detailed instructions on these steps, refer to Shopify's custom pixel management guide.

Step 5: Testing

  1. Test each scenario:
    • Place a one-time purchase order
    • Place a 1-month subscription order
    • Place a 3-month subscription order
  2. Check Convert reports to verify goals are triggering correctly
  3. Monitor browser console for debug messages during testing
  4. Verify pixel status in Shopify Admin → Settings → Customer Events

Step 6: Adding More Subscription Plans

To add additional subscription plans, extend the mapping:
 
const convertGoalsMapping = {
// ... existing plans ...
sixMonthSubscription: {
goalId: "YOUR_SIX_MONTH_GOAL_ID",
sellingPlanId: "YOUR_SIX_MONTH_SELLING_PLAN_ID",
},
annualSubscription: {
goalId: "YOUR_ANNUAL_GOAL_ID",
sellingPlanId: "YOUR_ANNUAL_SELLING_PLAN_ID",
},
};
 
Then add the corresponding checks in the event handler:
if (convert.matchSubscriptionPlan(lineItems, convertGoalsMapping.sixMonthSubscription)) {
goalId = convertGoalsMapping.sixMonthSubscription.goalId;
}
if (convert.matchSubscriptionPlan(lineItems, convertGoalsMapping.annualSubscription)) {
goalId = convertGoalsMapping.annualSubscription.goalId;
}
 

Important Notes

 

Compatibility with Official Web Pixel

  • ✅ Safe to use alongside your official Convert Shopify Web Pixel
  • ✅ No conflicts - both pixels can subscribe to the same events
  • ✅ Shared visitor data - both pixels use the same Convert tracking cookies
  • ✅ No duplicate goal triggering - Convert's built-in goal state management prevents duplicates
 

Performance Considerations

  • The custom web pixel only runs on checkout_completed events
  • Minimal performance impact as it focuses only on goal triggering
  • No revenue tracking or complex calculations
 

Troubleshooting

 

Goals not triggering:

  • Verify your Convert Account ID and Project ID are correct
  • Check that Goal IDs exist in your Convert project
  • Ensure Selling Plan IDs match exactly (include the full gid://shopify/SellingPlan/ format)
  • Confirm that your subscription app creates native Shopify selling plans (not all apps do)
  • Verify that item?.sellingPlanAllocation?.sellingPlan?.id exists in checkout data
 

Console errors:

  • Check browser console for error messages
  • Verify the Convert class implementation is complete
  • Ensure all required constants (ExperienceData, CookieKey, etc.) are defined
 

Duplicate goals:

  • This is normal behavior - Convert prevents duplicate goal triggering automatically
  • Check console logs for "already triggered" messages

Support

If you need assistance implementing this custom web pixel:
 
  1. Contact Convert support with your specific subscription plan setup
  2. Provide your Convert Account ID and Project ID
  3. Include details about your Shopify selling plans and desired goal mapping
 

📒Note:

This custom web pixel complements your existing Convert integration and does not replace the official Shopify Web Pixel. Both can run simultaneously to provide comprehensive tracking coverage.