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:
- Get an overview of creating a custom Shopify web pixel
- Understand the main use cases
- Review prerequisites
- Gather required information
- Create the custom web pixel
- Configure your goal
- Save and Activate
- Test one-time and subscription
- Add more Subscriptions
- Troubleshoot issues
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

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:
-
Install the Shopify Subscriptions app from the Shopify App Store
-
Create subscription plans for your products
-
Ensure products have subscription options enabled
Finding the Selling Plan ID:
-
In your Shopify Admin, go to Apps → Subscriptions
-
Click on Subscription plans or Plans
-
Click on the specific subscription plan you want to track
-
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
-
-
The full selling plan ID format for the code will be:
gid://shopify/SellingPlan/1862795494
-
Repeat this process for each subscription plan you want to track
Alternative method using browser developer tools:
-
Go to your storefront and view a product with subscription options
-
Open browser developer tools (F12)
-
Go to the Network tab and reload the page
-
Look for GraphQL or API requests containing selling plan data
-
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
-
In your Shopify Admin, go to Settings → Customer Events
-
Click Add Custom Pixel
-
Enter a name (e.g., "Convert Subscription Goals")
-
Copy and paste the following code:

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
-
Save the custom web pixel in Shopify Admin
-
Connect your web pixel to your store (if prompted)
-
Set permissions - ensure the pixel has access to customer events
-
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
-
Test each scenario:
-
Place a one-time purchase order
-
Place a 1-month subscription order
-
Place a 3-month subscription order
-
-
Check Convert reports to verify goals are triggering correctly
-
Monitor browser console for debug messages during testing
-
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:
-
Contact Convert support with your specific subscription plan setup
-
Provide your Convert Account ID and Project ID
-
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.