The OpenAI product feed specification defines 9 required fields (item_id, title, description, url, brand, seller_name, image_url, availability, price), a set of recommended fields for variants, identifiers and sale pricing, and optional fields for attributes, media, fulfilment, ads and checkout. Values follow strict formats: money as 79.99 USD, booleans as true/false, identifiers as strings. Rows that break a rule are not usable, and the documentation describes no per-row error report.
Why the exact wording of the rules matters
The file upload overview lists the three most common failure causes as “missing required fields”, “outdated or non-spec field names” and “malformed field values”. All three are formatting problems: the product is fine, the row is not. Because the documentation describes no per-row error report, a merchant learns about a malformed row only by noticing that a product never appears in ChatGPT. That is why every rule below comes with a rejected example. Quoted text is verbatim from the product feed specification and its two companion pages listed in the sources.
The 9 required fields
A row that omits any of these, or supplies an empty or unrecognized value, is not usable. The specification is explicit for availability: omitted, empty or unrecognized values reject the row.
| Field | Type | Constraint (from the specification) | Valid example | Rejected example |
|---|---|---|---|---|
item_id | string | “Stable ID, unique per item or variant within your feed. Never reuse it for a different item.” | TRAIL-BLK-10 | A database row number that changes on re-import |
title | string | “Product name, including the selected variant when relevant.” Max 150 characters, plain text | Trail running shoes, black, size 10 | A 300-character title stuffed with keywords |
description | string | “Factual product description for this item.” Max 5,000 characters, plain text | Waterproof trail shoes with a rubber outsole and mesh lining. | <p>Waterproof <strong>trail</strong> shoes</p> |
url | URL | “Product detail page for the item, with the variant selected when possible. Keep it stable.” Absolute HTTP or HTTPS, publicly accessible | https://example.com/products/trail?color=black&size=10 | /products/trail (relative path) |
brand | string | “Product brand as shown on the product page.” A real brand, not a placeholder | Northline | n/a |
seller_name | string | “Name of the seller supplying this offer.” A real name, not a placeholder | Northline Outdoor | unknown |
image_url | URL | “Main product image, showing this variant. Use a direct image URL, such as a JPEG or PNG.” | https://example.com/images/trail-black.jpg | https://example.com/products/trail (a page, not an image) |
availability | enum | “in_stock, out_of_stock, pre_order, backorder, or unknown.” | in_stock | available, preorder, In Stock |
price | money | “Regular item price in major currency units.” Format amount CURRENCY | 79.99 USD | 79,99, $79.99, 79.99, 1,079.99 USD |
Three details in this table cause most of the trouble.
item_id must survive re-imports. An identifier generated from a row position or a timestamp makes every snapshot look like a new catalog. Use the SKU or the platform’s variant identifier, as a string.
availability is a closed list. Google Shopping uses preorder; OpenAI uses pre_order. A feed copied from a Google export carries the wrong spelling on every pre-order product.
price is one string, not two columns. The specification asks for “a decimal amount in major units, a space, and an uppercase three-letter ISO 4217 currency code”, with “a decimal point, no thousands separators or exponent notation, and no more fractional digits than the currency permits”. A price exported from a French locale as 25,99 fails on the decimal separator alone.
Recommended fields
These are not required, but a malformed value in a recommended field is still a malformed value. If you cannot populate one correctly, omit it.
Variants: group_id, listing_has_variations, variant_dict
The three fields work together. group_id is a “Stable parent-listing ID shared by all variants.” The specification adds: “Omitted or empty: uses item_id, which does not establish a variant group.” So a group_id copied from item_id produces no group at all, silently.
listing_has_variations must be set to true on every variant row: “Omitted, empty, or false: no variant options are used.”
variant_dict is a map of option names to selected values, as strings. It “Requires listing_has_variations=true and group_id different from item_id.” Keys and values must be nonempty, and the same option names must be used across the whole group with unique option combinations. The specification also asks you to keep top-level attributes such as color and size consistent with the same options in variant_dict, because “Neither representation reconciles conflicting values for you.”
| Situation | Result |
|---|---|
group_id = TRAIL, item_id = TRAIL-BLK-10, listing_has_variations = true, variant_dict = {"color":"Black","size":"10"} | Variant group established |
group_id = TRAIL-BLK-10 (same as item_id) | No variant group |
group_id set, listing_has_variations omitted | No variant options used |
variant_dict = {} | Treated as no options |
color = Blue at top level, variant_dict.color = Black | Conflicting values, not reconciled |
Identifiers: gtin, mpn, offer_id
gtin is “One assigned GTIN: exactly 8, 12, 13, or 14 digits, including a valid check digit. Preserve leading zeros; no spaces or dashes.” Two consequences: a GTIN with a typo fails the check digit, and a GTIN exported from a spreadsheet that dropped the leading zero has the wrong length. Validate before uploading.
mpn is the “Manufacturer-assigned part number, preserving its punctuation and casing.” The specification is blunt about a common shortcut: “do not invent a value to replace a missing GTIN.”
offer_id is a “Stable offer ID, unique within the feed. Use it to distinguish offers that share a product URL.” It is a string, so leading zeros are preserved.
Condition and sale price
condition accepts new, refurbished or used. Omitted or empty “may be treated as new”, so always specify used or refurbished when it applies.
sale_price is the “Current sale price: greater than zero, strictly less than price, and in the same currency.” A sale price that is “Nonpositive, equal, higher, or different-currency” is not used. The specification also tells you when to update it: “Submit the current price; update the feed when a sale starts or ends.” Do not pre-schedule a sale by sending the future price.
price | sale_price | Result |
|---|---|---|
79.99 USD | 59.99 USD | Used |
79.99 USD | 79.99 USD | Not used (equal) |
79.99 USD | 59.99 EUR | Not used (different currency) |
79.99 USD | 0.00 USD | Not used (nonpositive) |
is_eligible_search
“true enables search eligibility; false disables it and checkout eligibility. Omitted or empty: true.” This is the switch that removes a product quickly: the file upload overview recommends setting is_eligible_search=false to make a product ineligible in the next processing cycle, rather than simply dropping the row, because OpenAI “retains its most recently processed record for up to 14 days”.
Optional fields, grouped
Item attributes
| Field | Rule |
|---|---|
product_category | “Your category path, from broad to specific, separated by >”, for example Apparel & Accessories > Shoes |
material | Principal materials in the item |
color | Selected color, consistent with the product image |
size | Selected size label; use variant_dict when size distinguishes variants |
gender | male, female or unisex; anything else means no gender supplied |
age_group | newborn, infant, toddler, kids or adult; “a product attribute, not a purchase-age restriction” |
dimensions | Object with positive decimal strings for at least two of length, width, height, plus a unit (in, cm, ft, m, mm); an empty object is invalid |
weight and item_weight_unit | Positive net weight without packaging; unit g, kg, oz or lb; the unit is required with the weight |
Media
additional_image_urls is an array in JSON or Parquet, and a comma-separated string in CSV or TSV. Invalid URLs are omitted. If an image URL itself contains a comma, the specification asks you to “Percent-encode commas as %2C in URL”, otherwise the delimiter splits the URL in two.
Fulfilment and returns
| Field | Rule |
|---|---|
shipping_price | Money format, same currency as price, nonnegative; “zero = no charge. Omitted/empty = unknown, not free” |
shipping | Tuple country:region:service_class:price, keeping the empty region position, for example US::Standard:5.00 USD |
accepts_returns | true or false; omitted means unspecified |
return_deadline_in_days | Positive whole number, “Supply only with accepts_returns=true” |
return_policy | Public HTTP or HTTPS URL to the returns or final-sale terms |
Reviews and ratings
review_count is a nonnegative whole number of product reviews (not seller or store reviews); zero means no reviews, omitted means unknown. star_rating is a decimal string on a 0 to 5 scale with two decimal places, for example 4.50, and must be paired with a matching positive review_count.
Merchant information
seller_url points to the seller’s storefront or profile page (for marketplace offers, the specific seller’s page). marketplace_seller names the marketplace where checkout occurs and requires setup with OpenAI.
Ads
is_ads_eligible: “Set true for products Ads should process; false explicitly opts out. Omitted/empty: disabled unless feed-level default applies.” Set it explicitly rather than relying on a default. ads_metadata is a string-to-string object using keys configured for your Ads integration, for example {"custom_label_0":"summer"}; “Do not invent keys.”
Checkout
is_eligible_checkout “true opts in only when search eligibility also true and checkout enabled. Omitted/empty/false: disabled. Search=false overrides this.” When you opt in, publish seller_privacy_policy and seller_tos as public URLs; supplying them “does not establish checkout readiness” on its own.
Geo-tagging
target_countries is an array of uppercase ISO 3166-1 alpha-2 codes “configured for feed. Omitted/empty does not mean worldwide. Requires market setup.” So ["US"] is valid, ["United States"] or ["us"] is not. store_country is the seller’s store country as an ISO code, not a regional price or stock override.
Data rules that apply to every field
These conventions come from the “general conventions” section of the specification and apply regardless of file format.
- Omit unknown values. “Omit an unknown value. Unless a row says otherwise, an omitted field, JSON null, or an empty delimited cell supplies no value.”
- No placeholders. “Do not use placeholder strings such as null, unknown, or n/a; unknown is valid only where explicitly listed.” The only place
unknownis a legal value isavailability. - Booleans. “For boolean fields, use JSON true or false, or the lowercase strings true and false in delimited files.”
TRUE,1,yesandYare not booleans. - Decimals. Decimal point, no thousands separators, no exponent notation.
- Identifiers as strings. “Keep identifiers as strings to preserve leading zeros.” This matters for
gtin,item_id,offer_idandmpn, and most of all in Parquet, where a writer that infers an integer column drops the zeros. - Text and URLs. “Use UTF-8 text and absolute HTTP or HTTPS URLs; prefer HTTPS.”
- CSV quoting. “In CSV, quote a cell containing commas, quotes, or newlines, and double each embedded quote. JSON objects in CSV or TSV cells must be serialized as JSON.” A
variant_dictin a CSV cell looks like"{""color"":""Black"",""size"":""10""}". - Stability across updates. Keep
item_id,group_idandoffer_idstable when price, stock, title or images change.
Legacy aliases
Older names are still accepted, but the specification asks you to “Send only one name per value” and states which name wins when both are present: “item_id wins over id and sku; group_id wins over item_group_id; the enable_ flags win over their is_eligible_ names.” Use the current names and never emit the alias in the same file.
| Current name | Legacy alias |
|---|---|
item_id | id, sku |
group_id | item_group_id |
is_eligible_search | enable_search |
is_eligible_checkout | enable_checkout |
is_ads_eligible | is_eligible_ads |
return_deadline_in_days | return_window |
If your feed is derived from a Google Shopping export, the Google names (id, link, image_link, item_group_id) are the ones to translate. The comparison in Google Shopping feed vs OpenAI feed walks through that mapping.
A valid JSONL row
One line per item. This row uses the required fields, the variant fields and a few recommended and optional ones. Note the title uses a comma to separate the variant details, and every boolean is a JSON boolean, not a string.
{"item_id":"TRAIL-BLK-10","group_id":"TRAIL","listing_has_variations":true,"variant_dict":{"color":"Black","size":"10"},"offer_id":"northline-TRAIL-BLK-10","title":"Trail running shoes, black, size 10","description":"Waterproof trail shoes with a rubber outsole and mesh lining. Lace closure, 320 g per shoe.","url":"https://example.com/products/trail?color=black&size=10&utm_medium=feed","brand":"Northline","seller_name":"Northline Outdoor","image_url":"https://example.com/images/trail-black.jpg","additional_image_urls":["https://example.com/images/trail-black-side.jpg"],"availability":"in_stock","price":"79.99 USD","sale_price":"59.99 USD","gtin":"00012345678905","condition":"new","color":"Black","size":"10","product_category":"Apparel & Accessories > Shoes","is_eligible_search":true,"is_ads_eligible":true,"target_countries":["US"]}
The utm_medium=feed parameter on url follows the best practices page, which suggests adding “feed attribution parameters to url (for example utm_medium=feed)” so that clicks from the feed can be told apart in your analytics. The same page asks for “concise, factual copy” in titles and descriptions, and to keep “title, url, description, media, availability, and price variant-specific when those values differ”.
Common mistakes
- Exporting
priceandcurrencyas two columns, or with a locale decimal comma. - Copying
group_idfromitem_id, which produces no variant group. - Writing
TRUE/FALSEor1/0in boolean columns. - Filling
brandorseller_namewithn/ato pass a “required” check in an internal tool; the specification treats the placeholder as invalid. - Leaving HTML from the store editor inside
description. - Sending both
idanditem_idin the same file. - Letting a spreadsheet turn
gtininto a number and drop the leading zero. - Setting
is_eligible_checkout=trueon a product whoseis_eligible_searchisfalse.
How Convrail validates each rule before delivery
Convrail applies these rules before any file reaches OpenAI, so a broken row is reported to you instead of vanishing:
- The 9 required fields are checked for presence and format on every row: money as
amount CURRENCY,availabilityagainst the closed list,urlandimage_urlas absolute HTTP or HTTPS URLs pointing at a direct JPEG or PNG. Convrail also capsitem_idat 100 characters andbrand,seller_nameandmpnat 70, and rejects titles written entirely in capital letters. - Placeholder strings (
null,unknown,n/a) inbrandandseller_nameare rejected. - Conditional fields are validated when present:
sale_pricestrictly belowpricein the same currency,group_iddifferent fromitem_id,is_eligible_checkoutrequiring search eligibility plus the two policy URLs,target_countriesas uppercase alpha-2 codes,star_ratingbetween 0 and 5. - The GTIN check digit is verified. An invalid GTIN is omitted rather than sent, so the row stays valid; the omission is journaled.
- HTML is stripped from descriptions, and booleans are emitted as JSON booleans in JSONL and Parquet and as lowercase strings in CSV and TSV.
- Every rejection is recorded with the
item_id, the field and the rule, in a run journal that also counts items read, accepted and rejected.
The same catalog can also be projected into a Google-compatible feed. If your products are already missing from ChatGPT results, Why your products do not show up in ChatGPT shopping maps each symptom to a rule above, and Parquet vs JSONL vs CSV covers the file-level choices.
What to do next
Connect your store and let Convrail validate your catalog against every rule on this page before the first delivery: see the product feed page.
Sources
Frequently asked questions
How many fields are required in the OpenAI product feed?
Nine: item_id, title, description, url, brand, seller_name, image_url, availability and price. A row missing any of them, or carrying a malformed value in one of them, is not usable.
How do I write the price in an OpenAI product feed?
As amount, a space, then the uppercase ISO 4217 code, for example 79.99 USD. Use a decimal point, no thousands separators, no currency symbol, and no more fractional digits than the currency permits.
Does OpenAI still accept id, sku or item_group_id?
Yes, as legacy aliases for item_id and group_id, but the specification asks you to send only one name per value. Use the current names in any new feed and do not mix both in the same file.
Is sale_price allowed to equal price?
No. The specification requires sale_price to be greater than zero, strictly less than price and in the same currency; a sale price that is equal, higher, nonpositive or in another currency is not used.
What happens if group_id is the same as item_id?
No variant group is created. group_id must be a stable parent-listing identifier shared by all variants and different from every item_id, with listing_has_variations set to true on each variant row.