How do you send a product feed to ChatGPT Shopping?
ChatGPT Shopping does not read a tag on the product page. You push a catalog file to OpenAI over SFTP. For discovery, every row needs item_id, title, description, url, brand, seller_name, image_url, availability, and price.
What the file is
ChatGPT Shopping is the product results inside ChatGPT. OpenAI's commerce documentation treats the catalog as a product feed: a file you upload, not a crawl of the category page.
The get-started guide gives four steps. Read the file schema. Check the required fields on every record. Upload the catalog over SFTP. Overwrite that same file with a new snapshot. OpenAI recommends a full snapshot at least once a day. Discovery, ads, and checkout are separate. The field list is https://developers.openai.com/commerce/specs/file-upload/products. Delivery is https://developers.openai.com/commerce/specs/file-upload/overview.
Which file OpenAI accepts
The file-upload overview says to push the feed over SFTP. The preferred format is parquet. jsonl.gz, csv.gz, and tsv.gz are also supported. The file is UTF-8.
Keep one stable file name and overwrite it. If the catalog is split into shards, keep the same shard names. The overview suggests up to 500,000 items per shard and files under about 500 MB. URLs must be public, absolute, and preferably HTTPS. In CSV, quote a cell that contains a comma.
Do not guess the SFTP hostname. The file-upload overview does not publish one. The Ads guide returns host, port, and username from sftp_access for that feed. It does not say a discovery feed uses the same host.
The get-started guide says feed onboarding is currently for approved partners: https://developers.openai.com/commerce/guides/get-started. At https://chatgpt.com/merchants/, people who already applied are on a waitlist. Shopify and Etsy catalogs are already integrated. Shopping is live for ChatGPT users in the US.
The nine required fields
OpenAI marks nine fields as required for a useful discovery row. A required field must contain a value. An empty cell does not mean zero or false. Do not send the words null, unknown, or n/a, except for availability, where unknown is an allowed value.
item_id is a stable string, unique per item or variant. Never reuse it for a different item. The aliases id and sku are accepted, and item_id wins if both are sent. Keep it a string so a leading zero survives.
title is the product name, with the variant when that matters. Aim for at most 150 characters. description is plain text. Aim for at most 5,000 characters. url is the product page, with the variant selected when you can.
brand is the brand printed on the page. seller_name is the seller of this offer. image_url is a direct image file for this variant, not the HTML page.
availability is one of in_stock, out_of_stock, pre_order, backorder, or unknown. An omitted, empty, or unrecognized value rejects the row. unknown does not mean the item is in stock. Use it only when the stock status is actually unknown.
price is the regular price in major currency units: a decimal amount, a space, and an uppercase ISO 4217 code, as in 79.99 USD. Use a decimal point. No thousands separator. No currency symbol. For USD, use two decimal places. Do not round the price into a different payable amount.
Variants, identifiers, and sale price
Send one row per purchasable variant. The rows share a group_id and each has its own item_id. Set listing_has_variations to true, and send variant_dict as a map of option name to selected value, for example color and size. group_id must differ from each item_id. Each row carries its own price, availability, url, and image.
gtin is optional: 8, 12, 13, or 14 digits with a check digit, no spaces. Do not invent one. mpn is the manufacturer part number and goes with brand.
sale_price is optional. It must be above zero, below price, and in the same currency. A sale starts and ends when you update the file. The schema says dates in the file do not schedule that.
The same page says the standard upload currently targets the US. A target_countries column does not change that default by itself. Another market needs the integration confirmed.
79.99 PLN fits the price format: a decimal, a space, and an ISO 4217 code. It does not select Poland.
{"item_id":"MUG-350-BLUE","title":"Ceramic mug, 350 ml, blue","description":"Glazed ceramic mug with a handle. Dishwasher safe.","url":"https://example.com/products/mug-blue","brand":"Northline","seller_name":"Northline Home","image_url":"https://example.com/images/mug-blue.jpg","price":"18.00 USD","availability":"in_stock"}Shopify, WooCommerce, and a custom catalog
The schema has no column named after a platform. You map the catalog you already have onto these fields. One row is one variant a person can buy.
On Shopify, use a stable variant id as item_id and do not reuse it. Map title, plain-text body, variant URL, vendor, and shop name onto title, description, url, brand, and seller_name. The image must be a direct file URL. Quantity above zero is in_stock. Price is major units plus currency, not cents. The export is the Admin API or a product CSV, and its header row is not the OpenAI schema.
On WooCommerce, use a unique SKU as item_id. If SKUs repeat, use the variation id. The permalink is url. The regular price is price. Map instock to in_stock, outofstock to out_of_stock, and onbackorder to backorder. OpenAI spells pre-order as pre_order. Google Merchant spells it preorder. Copying a Google feed without that rename rejects those rows.
A custom store exports those columns from the database behind the product page. Build the file on the server. The file price and the HTML price should be the same number and currency.
How to tell that a row was accepted
Start with 100 items, every required field filled, then QA the first snapshot and overwrite the same file name.
A finished SFTP transfer is not an accepted row.
For Ads feeds, OpenAI documents upload statuses: scanning, received, processing, completed, completed_with_errors, skipped, and failed. Codes include missing_required_column, invalid_value, unsupported_file_type, and invalid_sftp_directory_layout. Files go in the SFTP root. The page is https://developers.openai.com/ads/product-feeds.
The file-upload guide names no screen of accepted and rejected rows. Read products through the commerce API: https://developers.openai.com/commerce/specs/api/overview. That API is not Ads. An Ads status is not the discovery result.
Errors that get a row rejected
A price written as 79,99 or 79.99PLN or $79.99 is not the documented form. availability written as preorder, in stock, or an empty cell is not in the allowed set. An image_url that points at an HTML page is not a direct image. A URL that is not public fails the access rule.
Reusing an item_id for a new product breaks the identity rule. Variant rows that share one item_id, or a group_id equal to the item_id, do not form a variant group. HTML inside description breaks the plain-text rule.
- One row per variant, with a stable item_id that is never reused.
- title, description, url, brand, seller_name, and a direct image_url on every row.
- availability is in_stock, out_of_stock, pre_order, backorder, or unknown.
- price looks like 18.00 USD, with a decimal point, a space, and an ISO currency code.
- The file is parquet, or jsonl.gz, csv.gz, or tsv.gz, UTF-8, and it overwrites the same name.
- The price in the file matches the price in the page HTML.
Can I send my Google Merchant feed unchanged?
Not as a blind copy. Several names differ. Google uses id, link, and image_link. OpenAI uses item_id, url, and image_url, and also requires seller_name. Google's preorder must become pre_order. Aliases exist for some older names, and item_id still wins over id.
Does the product page JSON-LD replace the feed?
No. The feed is the file OpenAI ingests for ChatGPT Shopping. schema.org Product and Offer describe the same facts on the page. They should match. One does not fill in the other.
How often should the file be replaced?
The get-started guide recommends a full snapshot at least once a day, written over the same file name. Update price and availability when they change. The schema says uploaded dates do not schedule those changes for you.
What if the variant is out of stock?
Send the row with availability out_of_stock and the current price. Dropping the row hides the variant. unknown is only for a stock status you do not have, and it does not mean in stock.