A direct product feed to ChatGPT is for approved partners only. OpenAI’s developer docs say feed onboarding “is currently available to approved partners”, and its merchant page says that if you’ve applied, “you’re on the waitlist”. Once approved, you upload a full snapshot over SFTP, ideally daily, with one row per purchasable item or variant and nine required fields.
Two warnings before you build anything. First, if you sell on Shopify or Etsy, OpenAI says your catalog “is already integrated, and no additional setup or application is required”. Second, OpenAI’s products page shows two schemas, Stable and Draft, and they disagree on almost everything. Build to Stable. OpenAI says Draft “is not supported for production integrations yet”, and yet its best-practices page uses Draft-style field names.
Everything below was checked on October 27, 2026. Re-check before building.
Do you need a direct feed at all?
| Your situation | What OpenAI says | What to do |
|---|---|---|
| Shopify store | Product data is “already integrated into ChatGPT through Shopify Catalog”. No additional work required | Check Shopify Catalog eligibility instead. See Shopify and ChatGPT |
| Etsy shop | Catalog already integrated; no application needed | Nothing to build for discovery |
| Other platform or custom stack | Apply to share your product feed; you join a waitlist | Apply, then prepare a Stable sample while you wait |
| Already crawled by ChatGPT | “Do I need a product feed if ChatGPT already crawls my site? No, but feeds give you greater control” | Your choice; a feed controls the data, not the result |
| Selling to shoppers outside the US | Shopping is “currently live for ChatGPT users in the U.S.”; standard uploads target the US | Not documented as live elsewhere; ask during onboarding |
Whether a Shopify store gains anything from adding a direct feed on top of Shopify Catalog isn’t documented in the pages we read. Don’t assume either way.
The approval and delivery path
| Step | What the docs say | Status |
|---|---|---|
| Apply | A form on OpenAI’s merchant page asks for your company, site, categories and unique SKU count, and whether your feed already meets OpenAI’s specification | Documented |
| Wait | Applicants are on a waitlist. A self-serve merchant portal is planned “later this year” | The waitlist is documented; the portal is announced only |
| Build a sample | Around 100 items, every required field in every row | Documented (file upload overview) |
| Deliver | Push over SFTP. Prefer parquet; jsonl.gz, csv.gz and tsv.gz also work. UTF-8. Keep the same file names and overwrite them | Documented |
| Keep it current | Full snapshot at least daily. Shards up to about 500,000 items or 500 MB | Documented |
| Remove a product | Set is_eligible_search to false, or leave it out of every shard. OpenAI keeps the last record for up to 14 days |
Documented |
| Approval criteria and timing | Not documented | Unknown |
OpenAI lists the three most common ingestion failures: missing required fields, “outdated or non-spec field names”, and malformed values. The second one is where the schema conflict bites.
The field map
These are the Stable fields. “Your source data” is described in general terms, because every store’s export is different. Check each against your own system.
Required on every row
| Feed field | Your source data | Rule to watch |
|---|---|---|
item_id |
A stable ID for the exact item or variant, such as its SKU | Unique, never reused, kept as a string so leading zeros survive |
title |
Product name plus the selected variant | Aim for 150 characters or fewer |
description |
Your product description, as plain text | “Factual”; aim for 5,000 characters or fewer |
url |
The product page, with the variant selected if possible | Public, stable |
brand |
The brand shown on the product page | A real brand, not a placeholder |
seller_name |
Who sells this offer | Required on every row |
image_url |
The main image of this variant | A direct, public JPEG or PNG link |
availability |
Current stock status | in_stock, out_of_stock, pre_order, backorder or unknown. Blank or unrecognized rejects the row |
price |
Regular price | 79.99 USD form: major units, a space, an ISO currency code |
Needed for variants
| Feed field | Your source data | Rule to watch |
|---|---|---|
group_id |
The parent product’s stable ID | Same on every variant; must differ from every item_id |
listing_has_variations |
— | true on every variant row |
variant_dict |
The selected options | An object like {"color":"Black","size":"10"}; same option names across the group |
offer_id |
Optional stable offer ID | Never include the price in it |
Optional, and worth sending if you have the fact
| Feed field | What it carries | Rule to watch |
|---|---|---|
sale_price |
The current sale price | Must be above zero and strictly below price, in the same currency, or it’s ignored |
gtin, mpn |
Assigned identifiers | GTIN of 8, 12, 13 or 14 digits with a valid check digit. Don’t invent an MPN to replace a missing GTIN |
dimensions, weight and item_weight_unit |
Product size and net weight | Product, not package. Units are never inferred |
material, color, size, age_group, condition |
Attributes | condition is new, refurbished or used; say used or refurbished explicitly |
review_count, star_rating |
Product review totals | Same review population; omit the rating when there are no reviews; exclude store reviews |
accepts_returns, return_deadline_in_days, return_policy |
Return terms | A policy link doesn’t set accepts_returns. Final sale: false, no window |
shipping_price |
Shipping charge | Omitted means unknown, not free |
One gap matters for spec-sold products: the Stable schema has no field for noise level, room coverage, runtime or most other performance facts. Those go in the title and description, as text with units. In the 10 Shopify Catalog humidifier records we saved on October 26, a decibel figure appeared in the description of 4 and in no title, so even merchants who measured it usually didn’t put it up front. That’s a different route from OpenAI’s feed, but the habit carries over.
A worked example: one product, two variants
This is a labelled hypothetical: a top-fill humidifier from an invented brand, “Example Home”, sold in white and black. Its regular price is 99.99 USD, it’s on sale for 79.99 USD, and the black one is out of stock. In Stable, each colour is its own row:
{"item_id":"EH-HUM4-WHT","group_id":"EH-HUM4","listing_has_variations":true,"variant_dict":{"color":"White"},"title":"Example Home top-fill humidifier, 4 L, white","description":"Top-fill ultrasonic humidifier. 4 L tank. Rated 26 dB on the low setting. Covers rooms up to 450 sq ft. Up to 30 hours per fill on low.","url":"https://example.com/products/hum4?color=white","brand":"Example Home","seller_name":"Example Home","image_url":"https://example.com/images/hum4-white.jpg","availability":"in_stock","price":"99.99 USD","sale_price":"79.99 USD"}
{"item_id":"EH-HUM4-BLK","group_id":"EH-HUM4","listing_has_variations":true,"variant_dict":{"color":"Black"},"title":"Example Home top-fill humidifier, 4 L, black","description":"Top-fill ultrasonic humidifier. 4 L tank. Rated 26 dB on the low setting. Covers rooms up to 450 sq ft. Up to 30 hours per fill on low.","url":"https://example.com/products/hum4?color=black","brand":"Example Home","seller_name":"Example Home","image_url":"https://example.com/images/hum4-black.jpg","availability":"out_of_stock","price":"99.99 USD","sale_price":"79.99 USD"}
We checked each row against the Stable rules: nine required fields present, group_id differs from both item_id values, listing_has_variations is true, option names match across the group, sale_price is below price in the same currency, and the black row says out_of_stock rather than leaving availability blank. The facts a shopper filters on (noise, room size, runtime, fill style) sit in the first two sentences of the description, each with its unit and setting.
The price mapping is the step most likely to go wrong. If your store keeps a “compare-at” price, that is your regular price only if it really was the regular price. Stable puts it in price and the lower current price in sale_price. Shopify, for its own catalog, sends compare-at prices as list_price, which is the Draft and protocol name. Same fact, different field, depending on the route.
The schema conflict, field by field
Here is the same product described by OpenAI’s two schemas. Stable is supported. Draft is “for planning and feedback only”. OpenAI’s products API, which reads a feed back, returns the Draft-like nested shape.
| Topic | Stable (build to this) | Draft and API (don’t upload this) | Likely result if you mix them (our reading) |
|---|---|---|---|
| Record shape | One flat row per item or variant | One product object per line with a variants array, plus a header file |
Rows that don’t match the Stable contract, the “malformed values” failure OpenAI lists |
| IDs | item_id, group_id |
Product id, variant id |
id is accepted in Stable only as an old alias for item_id |
| Options | variant_dict object |
variant_options list of name and value pairs |
Variant options not read, because variant_options isn’t a Stable field |
| What “price” means | The regular price; sale_price is lower |
The active sale price; list_price is the pre-discount price |
Your sale price uploaded as your regular price, or the reverse |
| Money format | "79.99 USD" |
{"amount": 7999, "currency": "USD"}, integer minor units |
A price 100 times too high if minor units are copied into Stable’s major-unit format |
| Availability | Required string: pre_order, unknown and others |
Optional object: a true or false flag, and a status including preorder and discontinued |
preorder and discontinued aren’t Stable values, and Stable rejects a row with an unrecognized availability |
| Condition | new, refurbished or used | A list such as new or secondhand | “secondhand” isn’t a Stable value |
| Brand | Required | No brand field in the Product or Variant tables | A required field left empty |
| Seller | seller_name, required |
seller.name, optional, with policy links |
A required field left empty |
| Description | Plain text string | Object with plain, HTML or Markdown | A JSON object where text was expected |
| Images | image_url plus additional_image_urls |
media list; first entry is primary |
Missing required image |
The best-practices page is where builders get caught. It tells you to use optional fields like description.html, description.markdown, categories.taxonomy and seller.links, and to “populate variant_options”. Those are Draft and API names. None of them is a Stable field. If you follow that page into a Stable file, you’re producing exactly the “outdated or non-spec field names” OpenAI lists as a common failure.
The alias trap
Stable still accepts some older column names, but the tie-break isn’t consistent. Per the compatibility notes:
| If you send both | The one that wins |
|---|---|
item_id and id or sku |
item_id (the current name) |
group_id and item_group_id |
group_id (the current name) |
is_eligible_search and enable_search |
enable_search (the old name) |
is_eligible_checkout and enable_checkout |
enable_checkout (the old name) |
return_deadline_in_days and return_window |
return_window (the old name) |
So, by those rules, a feed that kept a legacy enable_search=false column while adding is_eligible_search=true would stay out of search. Send one name per value, and make it the current one.
Unknown is not zero, in the feed too
The Stable schema is unusually clear on this. “An empty value does not mean zero or false.” A blank shipping_price means unknown, “not free shipping”. A review_count of 0 means no reviews; a blank one means unknown. Don’t type placeholder strings like n/a or null into a cell; unknown is allowed only where the spec lists it, as with availability.
We found the same pattern in Shopify’s catalog: of 1,620 results from 162 searches on October 26, 440 had no product rating and none had an explicit zero. Our review-data audit shows how easy it is to turn those blanks into zeros by accident. Your feed export can do the same thing. Check that it leaves unknowns blank.
Checkout, ads and markets are separate switches
- Search is on by default.
is_eligible_search=falseturns off search and checkout for that row. OpenAI says eligibility “does not guarantee display”. - Checkout needs “a separately enabled integration”; the flag alone doesn’t do it. OpenAI’s merchant page also says it is moving away from a standalone Instant Checkout, with purchases completed on merchant sites or apps.
- Ads use
is_ads_eligibleand a separate Ads feed guide. Ads are independent of search eligibility. - Markets: standard uploads target the US. The format can represent US, CA and MX, but other markets need OpenAI to confirm your integration. A currency or URL doesn’t select a market.
For how the feed fits OpenAI’s wider commerce protocol, see UCP vs ACP. For identifiers, see GTIN, MPN and SKU. For variant modelling on your own pages, see variant structured data.
How we checked
On October 27, 2026 we downloaded OpenAI’s product file upload page and read both its Stable view and the Draft fragment the page loads, plus OpenAI’s file upload overview, get-started guide, best-practices guide, API products reference and merchant page. The conflict table compares field names, types and allowed values across those pages as they read that day. We didn’t upload a feed or call the API. The humidifier and review counts come from our saved Shopify Catalog snapshot of October 26, 2026, not from ChatGPT.
Your next step
Before you write a line of export code, open OpenAI’s products page and confirm the toggle says Stable. Then take one real product with variants and write its rows by hand against the field map above. Check four things: every row has the nine required fields, price is the regular price, no Draft or best-practices field names crept in, and the fact your customers ask about most is in the first sentence of the description, with its unit.