# Why your products do not show up in ChatGPT shopping results: a 10-point checklist

> Products missing from ChatGPT shopping usually trace to a malformed feed row. Ten causes, from a bad price format to a stale snapshot, and how to fix each.

Canonical: https://convrail.com/blog/products-not-showing-chatgpt-shopping/

Products missing from ChatGPT shopping results almost always trace to a feed row that OpenAI could not use: a required field is missing, a price is written `25,99` instead of `25.99 EUR`, a brand says `n/a`, a GTIN fails its check digit, or the snapshot stopped refreshing. The specification documents no per-row error report, so you have to audit the file yourself. Here are the 10 causes to check, in order.

## Why you have to audit the file yourself

The [file upload overview](https://developers.openai.com/commerce/specs/file-upload/overview) lists three common failure causes: "Missing required fields", "Outdated or non-spec field names" and "Malformed field values". It also asks you to "Start with a small sample (around 100 items)" before a full deployment. What the documentation does not describe is a report that tells you which rows were rejected and why. Until such a report exists, a product that disappears from ChatGPT is a symptom you have to trace back to a row in your file.

This checklist goes from the most frequent row-level causes to the file-level causes that take a whole catalog offline at once. For each, you get a detection method you can run on your own export and the fix. Field rules are quoted from the [product feed specification](https://developers.openai.com/commerce/product-feeds/spec); the full field reference is in [The OpenAI product feed specification, explained field by field](/blog/openai-product-feed-spec-explained/).

## 1. A required field is missing or empty

The nine required fields are `item_id`, `title`, `description`, `url`, `brand`, `seller_name`, `image_url`, `availability` and `price`. The specification says an omitted field, a JSON null or an empty delimited cell "supplies no value", and for `availability` it states that omitted, empty or unrecognized values reject the row.

**How to detect.** Count empty cells per required column in your export. The usual culprits are `brand` (empty vendor field on Shopify, empty attribute on WooCommerce), `description` (products created quickly with a title only) and `seller_name` (never mapped because no other channel asks for it).

**How to fix.** Fill the value at the source. Do not fill it with a placeholder to pass the check; that is cause number 4.

## 2. Outdated or non-spec field names

Feeds derived from an older OpenAI template or from a Google Shopping export carry names OpenAI treats as legacy aliases (`id`, `sku`, `item_group_id`, `enable_search`, `enable_checkout`, `is_eligible_ads`, `return_window`) or names it does not know at all (`link`, `image_link`, `google_product_category`). Aliases are still accepted, but the specification asks you to "Send only one name per value", and unknown names supply nothing, so a required field spelled the Google way is a missing required field.

**How to detect.** Compare your header row (or your JSON keys) to the current names: `item_id`, `group_id`, `is_eligible_search`, `is_eligible_checkout`, `is_ads_eligible`, `return_deadline_in_days`, `url`, `image_url`.

**How to fix.** Rename in the export mapping, and never emit both the current name and its alias in the same file.

## 3. Price or sale price in the wrong format

`price` is one string: "a decimal amount in major units, a space, and an uppercase three-letter ISO 4217 currency code", for example `79.99 USD`, with "a decimal point, no thousands separators or exponent notation". `sale_price` must be "greater than zero, strictly less than price, and in the same currency"; an equal, higher, nonpositive or different-currency sale price is not used.

**How to detect.** Search the price column for a comma, a currency symbol (`€`, `$`, `£`), a lowercase currency code, a missing currency, or a value above 999 written with a thousands separator. Then compare `sale_price` to `price` row by row and flag every case where they are equal.

**How to fix.** Format prices in the export, not in the store. A store that displays `1 079,99 €` is fine; the feed must say `1079.99 EUR`. A sale that ended should remove `sale_price` from the row: "Submit the current price; update the feed when a sale starts or ends."

## 4. Placeholder strings

"Do not use placeholder strings such as null, unknown, or n/a; unknown is valid only where explicitly listed." The only field where `unknown` is legal is `availability`. Placeholders usually appear in `brand` and `seller_name` because an internal tool insisted the columns be non-empty.

**How to detect.** Search every text column for `null`, `unknown`, `n/a`, `na`, `none`, `-` and `TBD`, case-insensitive.

**How to fix.** Replace with the real value. For `brand`, the specification wants the "Product brand as shown on the product page"; for private-label products, that is your own brand.

## 5. HTML, all-caps or over-long titles and descriptions

`title` is limited to 150 characters and `description` to 5,000, both plain text. The [best practices page](https://developers.openai.com/commerce/guides/best-practices) asks for "concise, factual copy that helps users understand products. Plain text and bullet-style text are both acceptable." Store editors save descriptions as HTML, and an export that does not strip tags ships `<p>`, `<br>`, `&nbsp;` and inline styles inside the field.

**How to detect.** Search the description column for `<` and `&`. Measure title length and flag anything above 150 characters. Flag titles where every letter is a capital.

**How to fix.** Strip tags and decode entities in the export step, keep the paragraph breaks as line breaks. Rewrite titles in sentence case with the variant details included, since the specification wants the "Product name, including the selected variant when relevant." The specification does not name all-caps as a rejection cause; Convrail's validator rejects it anyway, because an all-caps title reads as shouting inside a conversational answer and is never how a real product page names the product. If you want help rewriting titles at scale, the [feed optimization](/feed-optimization/) module drafts them for your review.

## 6. The image URL is not a direct JPEG or PNG

`image_url` must be a "direct image URL, such as a JPEG or PNG", publicly accessible, HTTPS preferred. Three patterns fail: a link to the product page instead of the image file, a URL that needs a session or a signed token to load, and a format other than JPEG or PNG (WebP or AVIF served by an image CDN by default).

**How to detect.** Fetch a sample of image URLs from outside your network with no cookies and check the `Content-Type` header: it should be `image/jpeg` or `image/png`. Flag URLs ending in `.webp` or `.avif`, and URLs containing `?token=` or `&signature=`.

**How to fix.** Point `image_url` at the original JPEG or PNG asset. Most platform CDNs keep the original when you request the URL without a format parameter.

## 7. Invalid GTIN

The `gtin` must be "exactly 8, 12, 13, or 14 digits, including a valid check digit. Preserve leading zeros; no spaces or dashes." A wrong GTIN is a malformed value on an otherwise good row.

**How to detect.** Three checks: length in {8, 12, 13, 14}; digits only (no hyphens, no spaces); and the check digit itself, which you can recompute with the standard modulo-10 weighting (multiply alternating digits by 3 and 1 from the right, sum, and the check digit brings the total to a multiple of 10). Also look for 11-digit or 12-digit codes that used to be 12 or 13: a spreadsheet that treated the column as a number dropped the leading zero.

**How to fix.** Correct the barcode at the source, or omit the field. The specification says explicitly "do not invent a value to replace a missing GTIN." Convrail verifies the check digit on every row and omits an invalid GTIN rather than sending it, so the product still ships and the omission is journaled.

## 8. Variant grouping errors

Variants need three fields to agree. `group_id` is the "Stable parent-listing ID shared by all variants"; if it is "Omitted or empty: uses item_id, which does not establish a variant group." `listing_has_variations` must be `true` on every variant row. `variant_dict` maps option names to values and "Requires listing_has_variations=true and group_id different from item_id."

**How to detect.** Flag rows where `group_id` equals `item_id`. Flag groups where `listing_has_variations` is missing or `false` on some rows. Flag groups whose `variant_dict` keys differ between rows (one variant says `colour`, the next says `color`) or where two rows share the same option combination. Compare top-level `color` and `size` with the same keys in `variant_dict`; the specification warns that "Neither representation reconciles conflicting values for you."

**How to fix.** Use the platform's parent product identifier as `group_id` and the variant identifier as `item_id`. Emit the same option names across the group. Keep `title`, `url`, `image_url`, `availability` and `price` variant-specific, as the best practices page recommends.

## 9. Eligibility and targeting flags

Several flags can hide a product on purpose or by accident.

- `is_eligible_search=false` "disables it and checkout eligibility". If a bulk edit set it on the whole catalog, nothing is eligible.
- `is_ads_eligible` "Omitted/empty: disabled unless feed-level default applies." Products you expect in ChatGPT ads need `true` explicitly.
- `is_eligible_checkout=true` is ignored when search eligibility is `false`, and the two policy URLs (`seller_privacy_policy`, `seller_tos`) do not by themselves establish checkout readiness.
- `target_countries` must be uppercase ISO 3166-1 alpha-2 codes "configured for feed. Omitted/empty does not mean worldwide." A country name, a lowercase code, or a country not configured in your setup does not target anything.

**How to detect.** Count `false` values in `is_eligible_search`. List distinct values in `target_countries` and compare them to what is configured with OpenAI. Check that boolean columns contain only `true` or `false` (in delimited files, lowercase strings; in JSONL and Parquet, real booleans), not `TRUE`, `1` or `yes`.

**How to fix.** Set the flags deliberately from feed settings rather than per product, and keep them out of spreadsheet formulas that output `TRUE`.

## 10. The snapshot is stale, oversized or renamed

The last group of causes takes a whole catalog offline rather than one product.

- **Not refreshed.** The overview asks you to "Publish full snapshots on a predictable cadence (at least daily)." OpenAI "retains its most recently processed record for up to 14 days", so a feed whose cron job died stays visible for a while, then everything expires at once. Conversely, a product you removed from the file lingers up to 14 days; to remove it sooner, keep the row and set `is_eligible_search=false`.
- **Wrong file naming.** "Use a stable file name. Keep the same file name on every update and overwrite it with the latest snapshot instead of creating a new name each run." A run that writes `feed-2026-09-05.jsonl.gz` next to yesterday's file does not replace yesterday's snapshot.
- **Oversized shards.** "Up to 500k items per shard is recommended; target shard files under ~500MB". Split before those limits.
- **Not UTF-8.** A Windows-1252 export with accented characters is a malformed value on every affected row.
- **Full deployment without a sample.** The overview asks you to start "with a small sample (around 100 items)". A hundred rows expose every mapping error with a fraction of the noise.

**How to detect.** On your SFTP destination, list the files: there should be one stable name per shard, overwritten daily, each under the size limit. Check the last modification time. Run `file` on the export to confirm UTF-8.

**How to fix.** Fix the scheduler and naming in the export job. Convrail writes shards as `feed-organic-000.<ext>`, `feed-organic-001.<ext>` and so on, splits at 500,000 items or about 450 MB, and overwrites the same names on every daily delivery; the format trade-offs are covered in [Parquet vs JSONL vs CSV for OpenAI product feeds](/blog/parquet-vs-jsonl-vs-csv-openai-feed/).

## Common mistakes when diagnosing

- Checking the store, not the file. The product page looks perfect; the exported row is what OpenAI reads.
- Testing one product and generalizing. A cause like a decimal comma affects every row exported from the same locale.
- Deleting a product from the feed to "reset" it. It persists up to 14 days; the reset does nothing visible.
- Fixing the value in the file by hand. The next automated snapshot overwrites the fix; correct the mapping or the source data.
- Assuming silence means success. With no per-row report described in the documentation, a delivery that uploaded without error tells you the transfer worked, not that the rows were usable.

## Where the Convrail run journal fits

Convrail runs this checklist automatically on every row before the file leaves. Each run records `itemsTotal`, `itemsValid` and `itemsRejected`, the diff against the previous run (added, removed, changed), the delivery status with the attempt count, and one error entry per rejected item with the `item_id`, the field and the rule it broke. The nine required fields, the price and sale price formats, placeholders, HTML in descriptions, all-caps titles, image URLs, the GTIN check digit, the variant rules and the eligibility flags are all covered, and the shards are named, sized and delivered as the overview asks. When a product you sell every day is not in ChatGPT, the journal tells you which run dropped it and why, instead of leaving you to reverse-engineer a silence. A failed delivery retries, then opens a [health alert](/health-alerts/).

## What to do next

Run your catalog through Convrail's validator and read the rejection journal before your next snapshot: see the [product feed page](/product-feed/).