OpenAI enabled advertising in ChatGPT this year. If you’ve bought media on any of the major platforms, the campaign side of ChatGPT ads feels familiar, but very limited. Targeting options are thin, there are only a couple of campaign objectives, and reporting is nowhere near what you’d get from Google or Meta.
The technical side is where it gets difficult. There are Shopify apps, but nothing established, no native feed integration, and the documentation is still underdeveloped. We’ve set up OpenAI ads tracking and product feeds for eCommerce clients on Shopify. This meant coding most of the conversion pixels ourselves and building the product feeds by hand.
Everything below is the process for getting ChatGPT ads running on a Shopify store. If you’re on another platform the OpenAI dashboard steps are the same, but the pixel and the feed will need reworking.
Here’s how to set up your own OpenAI ads account, from scratch all the way to a live campaign.
Step 1: Create your data source in the OpenAI ads dashboard
Start at ads.openai.com. In the left panel, go to Tools > Conversions > Data Source > Create. Name the data source something descriptive like “Shopify Web Store”. Set the type to “web” and turn on Automatic Advanced Matching.
Once you save, you’ll get a Pixel ID, a block of setup code, and an example event call. You mainly just need the Pixel ID for the next step.
Step 2: Install the OpenAI pixel in Shopify
Of the pixel ID, setup code, and example event call, you only need the pixel ID for Shopify. OpenAI’s setup code doesn’t work with Shopify’s event model, so we replaced it with our own.
In the Shopify admin, go to Settings > Customer Events > Add a Custom Pixel (top bar), then name it “OpenAI” or something similar. Under permissions, check Required and everything below it.
In the box for the code, use the JavaScript code below and replace the placeholder at the top with the real pixel ID you generated in Step 1.
{PIXEL CODE IN THE APPENDIX}
Once you save the JavaScript code into the box with your unique OpenAI pixel ID, you can save and it will connect the pixel to your live site.
NOTE: Right under where you change the pixel ID, there is a spot that says “debug: true”. That is so we can test the pixel before the final steps. After testing, you will need to change the “true” to “false”.
Step 3: Create the conversions that are tracked
Now that the pixel is firing, you can setup conversion tracking back in the OpenAI dashboard. Go to Tools > Conversions > Conversion Event > Create > Conversion Event.
For each of these, the base event is what step of the funnel you want to track. Give each of these a name which you’ll see in reporting. Set the data source to the one you made in step 1, e.g. “Shopify Web Store”.
We created five (following the format Name from `base event`):
- Page View from `page_viewed`
- Item View from `contents_viewed`
- Add to Cart from `items_added`
- Checkout Started from `checkout_started`
- Purchase from `order_created`
Step 4: Test the pixel, then turn off debug mode
The JavaScript from Step 2 should already be tracking the events in step 3. Go to Tools > Conversions > Event Stream > Start Polling. This will start tracking events on the site, so you can go through your storefront in another tab and trigger the various events to confirm they are firing. For example, view a page, view an item, add it to cart, start checkout, then place an order. Each of these should show up in the polling.
Once the test is complete, go back to the code in the Shopify custom pixel and change the “debug: true” to “debug: false” and save.
Step 5: Build the ChatGPT ads product feed
There are a few Shopify apps starting to appear, but nothing established yet, so we built the product feed manually. We expect proper feed apps to roll out soon, so check what’s available before doing this by hand. For now however, the product feed is just a CSV that you can upload yourself.
The CSV is just a simple grid, where each row is a product variant, and each column is a product feature.
- Core: item_id, title, description, url, brand
- Images: image_url, additional_image_urls
- Pricing: price, sale_price, sale_price_start_date, sale_price_end_date
- Stock: availability, condition
- Identifiers: gtin, mpn, product_category
- Variant attributes: material, color, size, size_system, gender, age_group, group_id, weight, item_weight_unit
- Seller info: seller_name, seller_url, seller_privacy_policy, seller_tos, return_policy
- Geo: target_countries, store_country
- Eligibility: is_eligible_search, is_eligible_checkout, is_ads_eligible
That’s all 35 columns, but only the 15 in bold are required. “is_ads_eligible” defaults to TRUE unless you specify FALSE.
The full reference is here https://developers.openai.com/ads/product-feeds, which came out after we built our setup.
A few things caused us a lot of trouble.
The item_id can’t have underscores. Shopify IDs come through like “Shopify_US_1234567_89012345”, so that’ll fail on upload. Just shorten it to the variant ID, which is the final string of numbers at the end, e.g. “89012345”. This item_id needs to match the content ID in the JavaScript from step 2, so also make sure your pixel is reporting back the variant ID, not the entire product ID.
The seller info columns (seller_name, seller_url, seller_privacy_policy, seller_tos, return_policy) are all live URLs, i.e. they have web pages on your store. If they don’t point to real pages, the entire row will be rejected.
The price column is just one field, with the currency code attached, e.g. “19.99 USD”. There isn’t a separate currency column. The availability column is lowercase with an underscore, e.g. in_stock or out_of_stock. The target_countries and store_country fields accept two letter codes, e.g. US, GB, AU, CA, etc. The three Eligibility fields accept TRUE or FALSE.
Step 6: Upload the feed
Go to the OpenAI dashboard, upload the CSV, and let it process. Once it’s accepted, we advise you to check a few products against the store to make sure prices, availability, and images come through how you expect.
One big thing to plan for: feed items expire after two weeks. So you’ll have to re-upload the product feed every two weeks. OpenAI recommends a hosted URL or automated SFTP uploads instead of doing it manually every time.
Step 7: Run a campaign
When you create your first campaign, set the mode to “product feed” and attach the feed you uploaded in step 6. You can’t change the mode after creating. Set your targeting and budgeting, and you’re ready to start running ads!
What’s still manual
The product feed. Every time your products change, or every two weeks, whichever comes first.
The pixel is solved though. Once you write it and add it to your Shopify site, it’s just copy and paste for any other clients you want to add, as long as you remember to swap out the pixel ID for each unique account.
Appendix: the code
A few weird notes about the code:
- Advanced matching hashes the customer’s email at checkout and calls init again with the hash attached, which is why the pixel ID gets referenced three times in the script. It’s set as a variable at the top, so you only have to change it once, e.g. `const OAI_PIXEL_ID = “CLIENTS-PIXEL-ID-GOES-HERE”`.
- Amounts go to OpenAI in cents, not dollars, so we multiply our price by 100 and round.
- Shopify can fire duplicate add to cart events for the same variant back to back, depending on the theme, so we de-dupe this issue within our script by skipping anything that repeats within 1.5 seconds.
- The purchase event passes the Shopify order ID as an event ID, which is what OpenAI dedupes against. Without it, someone refreshing the thank you page counts as two orders.
// ---- OpenAI Pixel init ----
// ---- Set your pixel ID here. This is the only place you need to change it. ----
const OAI_PIXEL_ID = "PLACE CLIENT ID HERE";
// ---- Toggle to false when done testing the pixel ----
const OAI_DEBUG = true;
!function(w, d, s, u) {
if (w.oaiq) return;
var q = function() { q.q.push(arguments); };
q.q = [];
w.oaiq = q;
var j = d.createElement(s);
j.async = 1;
j.src = u;
var f = d.getElementsByTagName(s)[0];
f.parentNode.insertBefore(j, f);
}(window, document, "script", "https://bzrcdn.openai.com/sdk/oaiq.min.js");
oaiq("init", {
pixelId: OAI_PIXEL_ID,
debug: OAI_DEBUG
});
// ---- Helper: SHA-256 hash for email advanced matching ----
async function sha256Hex(value) {
if (!value) return null;
const normalized = value.trim().toLowerCase();
const encoder = new TextEncoder();
const data = encoder.encode(normalized);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
}
// ---- Page View -> page_viewed ----
analytics.subscribe("page_viewed", (event) => {
const context = event.context;
const url = context?.document?.location?.href || "";
const title = context?.document?.title || "";
const payload = {
type: "contents",
contents: [
{
id: url,
name: title,
content_type: "page",
quantity: 1
}
]
};
oaiq("measure", "page_viewed", payload);
});
// ---- View Content -> contents_viewed ----
analytics.subscribe("product_viewed", (event) => {
const variant = event.data.productVariant;
const product = variant?.product;
const price = Number(variant?.price?.amount || 0);
const currency = variant?.price?.currencyCode || "USD";
const payload = {
type: "contents",
amount: Math.round(price * 100),
currency: currency,
contents: [
{
id: String(variant?.id || product?.id || ""),
name: String(product?.title || variant?.title || ""),
content_type: "product",
quantity: 1
}
]
};
oaiq("measure", "contents_viewed", payload);
});
// ---- Add to Cart -> items_added ----
const recentAdds = new Map();
analytics.subscribe("product_added_to_cart", (event) => {
const line = event.data.cartLine;
const merchandise = line?.merchandise;
const quantity = Number(line?.quantity || 1);
const price = Number(merchandise?.price?.amount || 0);
const currency = merchandise?.price?.currencyCode || "USD";
const variantId = String(merchandise?.id || "");
const lineValue = Math.round(price * quantity * 100);
// Dedupe: skip if the same variant fired within the last 1.5 seconds
const now = Date.now();
const lastFired = recentAdds.get(variantId);
if (lastFired && now - lastFired < 1500) {
return;
}
recentAdds.set(variantId, now);
const payload = {
type: "contents",
amount: lineValue,
currency: currency,
contents: [
{
id: variantId,
name: String(
merchandise?.product?.title ||
merchandise?.title ||
merchandise?.sku ||
""
),
content_type: "product",
quantity: quantity
}
]
};
oaiq("measure", "items_added", payload);
});
// ---- Initiate Checkout -> checkout_started ----
analytics.subscribe("checkout_started", async (event) => {
const checkout = event.data.checkout;
// Advanced matching: hash and pass email if available
const emailHash = await sha256Hex(checkout?.email);
if (emailHash) {
oaiq("init", {
pixelId: OAI_PIXEL_ID,
email_sha256: emailHash
});
}
const total = Number(checkout?.totalPrice?.amount || 0);
const currency = checkout?.totalPrice?.currencyCode || "USD";
const contents = (checkout?.lineItems || []).map((item) => ({
id: String(item?.variant?.id || item?.id || ""),
name: String(item?.title || item?.variant?.title || ""),
content_type: "product",
quantity: Number(item?.quantity || 1)
}));
const payload = {
type: "contents",
amount: Math.round(total * 100),
currency: currency,
contents: contents
};
oaiq("measure", "checkout_started", payload);
});
// ---- Purchase -> order_created ----
analytics.subscribe("checkout_completed", async (event) => {
const checkout = event.data.checkout;
// Advanced matching: hash and pass email if available
const emailHash = await sha256Hex(checkout?.email);
if (emailHash) {
oaiq("init", {
pixelId: OAI_PIXEL_ID,
email_sha256: emailHash
});
}
const total = Number(checkout?.totalPrice?.amount || 0);
const currency = checkout?.totalPrice?.currencyCode || "USD";
const contents = (checkout?.lineItems || []).map((item) => ({
id: String(item?.variant?.id || item?.id || ""),
name: String(item?.title || item?.variant?.title || ""),
content_type: "product",
quantity: Number(item?.quantity || 1)
}));
const payload = {
type: "contents",
amount: Math.round(total * 100),
currency: currency,
contents: contents
};
const options = {
event_id: String(checkout?.order?.id || checkout?.token || Date.now())
};
oaiq("measure", "order_created", payload, options);
});
Piqued your curiosity?
Let’s talk about how we can grow your business.