A few weeks ago we sat down and built an OpenAI-compliant product feed for a WooCommerce shop. A D2C brand in the US market, a few dozen SKUs, a sensitive product category. The result is three weeks of work, seven plugin iterations, a string of dead ends, and in the end a setup that almost no one in the DACH region probably has running yet.
This post isn't a theoretical intro. It's the things that actually surprised us while building — and that you won't find in any of the tutorials you can google on the topic today.
Why this matters right now
OpenAI has been building its own shopping index since mid-2025. When someone asks ChatGPT "best wireless earbuds under 200 dollars" or "affordable hiking shoes for wide feet", product cards appear directly in the chat — with price, rating, image, and link. These cards don't come from Google Shopping. They come from a separate feed system that OpenAI is currently scaling up.
The official spec lives at developers.openai.com/commerce/specs/file-upload/products and describes exactly which fields a product needs in order for ChatGPT to display it correctly.
So far the system is mostly filled with big players: Walmart, Etsy, Target, Sephora. Plus every Shopify merchant, because Shopify offers the onboarding through its own integration. Anyone running a WooCommerce shop is mostly on the sidelines for now.
That's exactly the interesting point: it's still early enough that a small or mid-size shop can ride the first wave relatively painlessly.
What's often misunderstood
Before we go deeper, three things to set straight that almost every surface-level article on the topic gets wrong:
OpenAI doesn't have a Merchant Center. You can't sign up and register your shop yourself the way you can with Google. The path is direct contact via the onboarding form at developers.openai.com/commerce/guides/get-started. OpenAI reviews applications manually.
A publicly hosted JSON feed doesn't replace the onboarding. Even if your products sit under domain.com/openai-products.json in perfect structure, OpenAI won't index them automatically. The official ingest runs via SFTP push or the ACP API after the onboarding process.
The strategy is shifting right now. In March 2026, OpenAI started pivoting away from "instant checkout directly in ChatGPT" toward dedicated retailer apps inside ChatGPT. Walmart was the first to ink the deal. Translation: the feed still matters, but is_eligible_checkout=true is currently less of a holy grail than it was six months ago.
Still worth building — for reasons we'll get to.
The difference between Shopify and WooCommerce
If you're a Shopify merchant, the work is done. Shopify has the OpenAI integration directly in the admin, a few clicks, done. That's why the answer to "Who's already in the ChatGPT index?" is disproportionately Shopify stores.
There's nothing comparable for WooCommerce. No official connector, no OpenAI-WooCommerce bridge, no plugin marketplace standard. If you want in, you build it yourself.
At first glance that's a disadvantage. In practice it has two upsides we learned to appreciate while building:
You have full control over what actually ends up in the feed. In our setup, for example, we stripped UTM parameters from product URLs because the source data came from an existing Google Shopping feed and would otherwise have conversions from ChatGPT show up as "Google Shopping" in analytics. With Shopify you don't have that level of fine-grained control.
And you build up an understanding of how agentic commerce really works in parallel. That'll be its own specialty in the next two years, comparable to what Google Shopping was around 2014: a channel that few people understand technically.
The spec: what OpenAI really wants
The official Product Feed Spec lists around 60 fields. Most tutorials just enumerate them. We take a different approach and sort them by practical importance.
What's actually mandatory
is_eligible_search(boolean)is_eligible_checkout(boolean — likelyfalsefor you until onboarding completes)item_id(stable SKU)title(max 150 chars, no all-caps)description(max 5000 chars, plaintext)url(must return HTTP 200)brandimage_urlprice(with currency, e.g.54.90 USD)availability(in_stock,out_of_stock,pre_order,backorder,unknown)seller_name,seller_url,return_policytarget_countries,store_country
What most people forget but boosts visibility massively
gtin(EAN/UPC) — without it ChatGPT can't reconcile your products with other data sourcesproduct_categoryas a readable path with>separators — heads up: if you're coming from a Google Shopping feed, you have numeric IDs here. You need to map them, otherwise the field is uselessreview_countandstar_rating— strongest ranking boost in ChatGPT answerswarning— for sensitive categories (supplements, food, alcohol, cosmetics) even compliance-relevantage_groupfor age-related products
Fields that get more relevant with every spec iteration
unit_pricing_measure+base_measure— for comparison queries like "cheapest per ounce / per liter"materialfor structured filteringaccepts_returns,return_deadline_in_days,accepts_exchanges— trust signalsq_and_aandreviewsas detailed lists
Code: a minimal working endpoint
If you're building this in WordPress, the cleanest entry point is the template_redirect hook. Here's the abbreviated core of what our endpoint does:
add_action('template_redirect', 'render_openai_feed', 0);
function render_openai_feed() {
$path = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH);
if ($path !== '/openai-products.json') return;
// Check cache
$cached = get_transient('openai_feed_cache');
if (is_array($cached)) {
send_feed($cached);
}
// Build from source, e.g. WooCommerce or existing XML feed
$items = build_items_from_source();
set_transient('openai_feed_cache', $items, HOUR_IN_SECONDS);
send_feed($items);
}
function send_feed($items) {
status_header(200);
nocache_headers();
header('Content-Type: application/json; charset=utf-8');
header('X-Robots-Tag: noindex, follow');
header('X-Content-Type-Options: nosniff');
echo wp_json_encode($items, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
exit;
}
Looks trivial but is tricky in several spots. The status_header(200) line for example: WordPress flags URLs that don't match any internal permalink rule as 404 internally — and keeps that status even when your hook successfully returns JSON. Without this line, every crawler receives an HTTP 404 response and throws the feed away. You'll only find the bug when you check response headers via curl.
Insight 1: HTTP 404 despite valid JSON response
This is the point that cost us the most time. You practically won't find it in forum threads or "Custom Endpoints in WordPress" tutorials. In the browser the feed looked perfect — but Cloudflare logs showed 404s, and testing with curl -I indeed returned HTTP/2 404.
Cause: WordPress' request lifecycle sets the status early, based on permalink matching. If the path matches no rule, it gets marked 404 internally. Later output via template_redirect doesn't change that unless you explicitly call status_header(200).
Takeaway: anyone serving a JSON feed via template_redirect must override the status manually.
Insight 2: Reviews from third-party sources need multi-key matching
Our setup had a separate reviews feed in Google Product Reviews format. The reviews were deeply nested:
<reviews>
<review>
<ratings><overall min="1" max="5">5</overall></ratings>
<products>
<product>
<product_ids>
<skus><sku>PRODUCT-SKU-001</sku></skus>
<gtins><gtin>1234567890123</gtin></gtins>
<mpns><mpn/></mpns>
</product_ids>
</product>
</products>
</review>
</reviews>
First implementation: SKU as lookup key, done. Didn't work reliably because in the product feed sometimes the SKU was the item_id, sometimes the MPN, sometimes a custom format. Solution: index the review under all available IDs simultaneously (SKU, GTIN, MPN), and on match in the product feed try all three. Plus a dedup logic via signature so a review doesn't count three times just because it sits under three IDs.
You won't find this in any standard solution because most examples assume reviews and products come from the same source.
Insight 3: Google taxonomy IDs are worthless in OpenAI's world
If you're importing from an existing Google Shopping feed, your google_product_category contains numeric IDs like 1604 or 888. Those are Google Merchant Center Taxonomy IDs — a curated vocabulary.
The OpenAI spec explicitly wants a different format: Apparel & Accessories > Shoes > Athletic Shoes — with > separators and readable text.
Pass 1604 through naked and you have a field ChatGPT can neither filter nor understand. Our fix: an internal map with the most relevant IDs for the given category; everything else gets dropped rather than passed through raw.
$map = [
'1604' => 'Apparel & Accessories > Shoes > Athletic Shoes',
'888' => 'Apparel & Accessories > Clothing > Activewear',
'2492' => 'Home & Garden > Kitchen & Dining > Kitchen Tools',
// ...
];
The official Google taxonomy list has over 6,000 entries. We needed about 18 for our use case. Map selectively rather than fully.
Insight 4: UTM parameters skew attribution
Our source feed came from an established WooCommerce feed plugin and was primarily configured for Google Shopping. Meaning: every product URL had UTM parameters attached:
?utm_source=Google%20Shopping&utm_campaign=USA%20Feed&utm_medium=cpc
When ChatGPT serves these URLs to users and a click happens, traffic shows up in analytics as "Google Shopping CPC" — even though it came from ChatGPT. That skews your entire attribution.
OpenAI sets its own referrer (utm_source=chatgpt.com). So if you keep the UTMs from the source feed, you're sabotaging clean tracking. The clean variant: strip all tracking parameters (utm_*, gclid, fbclid, msclkid). ChatGPT will set its own.
$strip_prefixes = ['utm_', 'gclid', 'fbclid', 'msclkid', 'mc_eid'];
Insight 5: Cache stampede is real, ETag should be standard
When a feed sits at a public URL and multiple crawlers arrive at the same time, the cache may be empty and all requests trigger the build in parallel. With a 1 MB XML parser on a typical WordPress server that's enough to briefly take the server down.
Solution: lock mechanism via transients. If a build is running, other requests wait briefly and use the finished cache:
$lock = get_transient('feed_lock');
if ($lock) {
for ($i = 0; $i < 16; $i++) {
usleep(500000);
$cached = get_transient('feed_cache');
if (is_array($cached)) return $cached;
}
}
set_transient('feed_lock', '1', 30);
// ... build ...
delete_transient('feed_lock');
Plus: set an ETag header. On the next request crawlers send If-None-Match with the old ETag, and if nothing changed the server answers with 304 Not Modified and zero bytes of payload. Saves roughly 1 MB of bandwidth and server load per recrawl.
What doesn't work: .well-known/ai-commerce/
In several early tutorials and in our own first plugin draft there was an endpoint at /.well-known/ai-commerce/products.json plus a discovery manifest at /.well-known/ai-commerce/. The idea: a standardized discovery URL like /.well-known/security.txt or /.well-known/apple-app-site-association.
Sounds smart. Doesn't work. We checked both the OpenAI docs and the official ACP (Agentic Commerce Protocol) in detail: .well-known/ai-commerce/ is not a standard. OpenAI doesn't use this path anywhere, GPTBot doesn't crawl it automatically, and the only .well-known/ endpoints in the ACP ecosystem are JWKS keys for authentication — i.e. something entirely different.
If you have this in your setup, you've invented a pseudo-standard that impresses no crawler. Better drop it and maintain a canonical path at /openai-products.json.
llms.txt: what it does and doesn't
The llms.txt spec emerged in 2024 as a proposal by Jeremy Howard. Idea: a standardized markdown file in the webroot that gives AI models a curated overview of a website's key content.
Yoast SEO has the feature built in since version 23.x, RankMath is following. The file is typically generated automatically once a week and contains links to the most important pages — About, Contact, Policies, Shop page.
What llms.txt does: Brand context for ChatGPT. When someone asks "Who is [Brand]?", ChatGPT gets a clean, structured answer instead of the haphazard crawler synthesis. That has a measurable effect on the quality of ChatGPT answers about your brand.
What llms.txt doesn't do: Bridge to your product feed. Contrary to common claims, llms.txt is not an official discovery mechanism for product data. GPTBot does not necessarily follow its links into the shopping index.
Practical consequence: keep and maintain llms.txt, but don't mistake it for a "visibility booster for products". The feed path belongs either in robots.txt (as a comment or via clean crawl access) or directly in the OpenAI onboarding form.
robots.txt: the underrated lever
OpenAI uses four different bots, with different jobs:
- OAI-SearchBot — Indexes content for ChatGPT search results. The most important of the four. If you block it, your shop won't show up in ChatGPT search answers at all.
- GPTBot — Crawls for foundation model training. If you block it, your content won't flow into training but search results stay reachable. Since December 2025, OpenAI has been sharing crawl results between GPTBot and OAI-SearchBot to avoid duplicate crawling.
- ChatGPT-User — Activated when a user explicitly requests a URL ("show me product X on the website") or accesses via GPT Actions / Custom GPTs. According to docs, doesn't strictly obey robots.txt because user-initiated.
- OAI-AdsBot — Validates landing pages for ChatGPT ads. If you want to advertise on ChatGPT (now or in the future), this bot must reach your product pages or ad validation will fail.
The typical mistake in robots.txt configs for AI bots: people add a blanket User-agent: GPTBot / Allow: /. That overrides the more specific rules in the User-agent: * block — including the disallows for cart, checkout, and search results.
Robots.txt semantics: the most specific user agent wins completely, not per-line. Meaning: you have to repeat the disallows per bot, otherwise URLs you explicitly blocked for everyone else suddenly open up for the OpenAI bots.
User-agent: OAI-SearchBot
Disallow: /wp-admin/
Allow: /wp-admin/admin-ajax.php
Disallow: */cart
Disallow: */checkout
Disallow: */wishlist
Disallow: */my-account
Allow: /
It's redundant code, but that's how the protocol works.
What actually needs to happen for a WooCommerce shop
If you want to build this yourself, the pragmatic plan looks like this:
- Clarify your data source. Do you already have a Google Shopping feed via an established WooCommerce feed plugin? Use it as the source instead of reading directly from WooCommerce. Advantage: the data is already validated by Google's standard.
- Build your own plugin, don't try to hack existing ones. There's no clean OpenAI connector for WooCommerce right now. Solving it through existing feed plugins gives you either too little control or too many unintended side effects on Google Shopping. Better a thin custom plugin that's strictly read-only.
- Configure the endpoint cleanly.
/openai-products.jsonwith HTTP 200,X-Robots-Tag: noindex, follow, ETag, cache. No sitemap link, no footer link without nofollow — otherwise Googlebot hits it, which doesn't help and just creates Search Console warnings. - Extend robots.txt. Four OpenAI bots with allow rules, but with disallows repeated per bot. Leave the regular catch-all rules for Googlebot unchanged.
- Start the OpenAI onboarding. Fill in the form at developers.openai.com/commerce/guides/get-started with your feed URL as reference. That's the actual lever — everything else is preparation.
What most people get wrong on priorities
From our experience: anyone tackling the topic spends 80% of the time on the wrong things.
Common mis-prioritizations: JSON-LD Product schema on product pages, llms.txt maintenance, the exact format of every optional field, custom discovery endpoints under .well-known/.
What's actually important: clean endpoint with HTTP 200, GTIN for all products, readable categories, correct URLs without UTM junk, and filling out the OpenAI onboarding form.
Onboarding is the only way to actually get into the ChatGPT shopping index. Everything else is a prerequisite, not a substitute.
What happens if you do nothing
Honest answer: probably not much, in the short term. ChatGPT search isn't the channel that carries a business yet. Even big retailers report single-digit percentages of their traffic from ChatGPT.
But: the curve goes up sharply. Walmart was the first major retailer to go live with the app model in April 2026. Etsy and Sephora are following. In 12 to 24 months, agentic commerce will be a channel where the established players are no longer catchable from behind — similar to Google Shopping between 2014 and 2018.
Invest a weekend now and you're prepared. Wait and you'll later be applying for the spots others already occupy.
What we'd build differently next time
First: Don't try to cover all spec fields at once. Start with the 15 actual mandatory fields, plus GTIN, plus clean categories. Iterate the rest.
Second: Bake performance and caching in from day one. Even if the feed is built only once an hour — when it's built, it has to be fast.
Third: Take ETag and HTTP 304 seriously. Crawlers crawl more often than you think. Saving bandwidth and crawl budget pays off.
Fourth: Separate "discovery preparation" from "actual indexing". The public feed endpoint is preparation. It only becomes effective after OpenAI onboarding. This expectation needs to be clear to everyone involved.
Fifth: Don't do anything that could affect Google Shopping. That's still the most important paid channel. The OpenAI feed has to run completely orthogonal — own endpoint, own cache, own robots.txt sections, no interference with existing plugins.
Where this is going
OpenAI Commerce is in a transition phase right now. The file-upload spec is stable, the API variant is being built in parallel, the shift to retailer apps is underway. Anyone building today is building for a target state that will shift again in the next six months.
That's fine. The fundamentals stay the same: structured product data in a JSON format ChatGPT can read, plus clear discovery paths via robots.txt and onboarding. What changes are fields, optional extensions, and the exact ingest mechanism.
Once you've built a plugin that handles this cleanly, you have an architecture that survives every spec iteration of the next two years.
And along the way you build an understanding of how agentic commerce technically works — a discipline that will be as specialized in two years as conversion tracking or server-side tagging is today.