# Google Shopping feed vs OpenAI product feed: what transfers, what breaks

> Most Google Shopping columns map to the OpenAI feed; seller_name, eligibility flags, target_countries and pre_order do not. Mapping table and fixes.

Canonical: https://convrail.com/blog/google-shopping-feed-vs-openai-feed/

Most of a Google Shopping feed transfers to the OpenAI product feed: identifiers, titles, descriptions, prices, GTINs, brand and condition all have a direct counterpart. What breaks is the part Google never asked for: a `seller_name` on every row, lowercase string booleans for search, ads and checkout eligibility, `target_countries`, and the `pre_order` spelling. This guide maps the fields one by one.

## Two specifications, one catalog

Google Merchant Center and OpenAI both want a flat list of products with a stable identifier, a price in a currency, an image and a landing page. The vocabulary differs. Google calls the identifier `id` and the landing page `link`; OpenAI calls them `item_id` and `url`. Google groups variants with `item_group_id`; OpenAI uses `group_id`. The values are usually the same data, so a mapping layer handles most of the work.

The differences that matter are not renames. They are fields OpenAI requires that have no source in a Google feed, values OpenAI spells differently, and defaults that behave differently when a column is missing. The rest of this article works through each of those.

OpenAI also accepts a second format that keeps the Google column names. The specification lists it separately and adds one condition: "Use this format only after OpenAI confirms it for your registered feed." So there are really three paths: map your Google feed to the native OpenAI format, register a Google-compatible feed, or generate both from one catalog. The last option is what Convrail does, and the reasons become clear once the mapping is on the table.

## Field-by-field mapping

The table below pairs each common Google attribute with its OpenAI counterpart. "Transfers" means the value can be copied as is. "Rename" means only the column name changes. "Convert" means the value itself needs work.

| Google attribute | OpenAI field | Status | Notes |
| --- | --- | --- | --- |
| `id` | `item_id` | Rename | OpenAI: "Stable ID, unique per item or variant within your feed. Never reuse it for a different item." Google caps `id` at 50 characters; OpenAI still accepts `id` and `sku` as legacy aliases. |
| `item_group_id` | `group_id` | Rename | Both are a shared parent identifier, distinct from each item identifier. OpenAI accepts `item_group_id` as an alias. |
| `title` | `title` | Transfers | Both allow 150 characters and want the variant named in the title. |
| `description` | `description` | Transfers, check content | Both allow 5,000 characters. OpenAI wants plain text; HTML left by a store editor should be stripped. |
| `link` | `url` | Rename | Both require an absolute HTTP or HTTPS URL. OpenAI: "Product detail page for the item, with the variant selected when possible." |
| `image_link` | `image_url` | Rename, check format | Google accepts JPEG, WebP, PNG, GIF, BMP and TIFF. OpenAI wants a direct JPEG or PNG link; a `.webp` image that Google accepts will not do. |
| `availability` | `availability` | Convert | Google: `in_stock`, `out_of_stock`, `preorder`, `backorder`. OpenAI native: `in_stock`, `out_of_stock`, `pre_order`, `backorder`, `unknown`. The underscore in `pre_order` is the trap. |
| `availability_date` | `availability_date` | Transfers | ISO 8601 on both sides. |
| `price` | `price` | Transfers | Google's example is `15.00 USD`; OpenAI's is `79.99 USD`. Same shape: decimal amount, space, uppercase ISO 4217 code. |
| `sale_price` | `sale_price` | Transfers, check value | OpenAI: "greater than zero, strictly less than `price`, and in the same currency." A sale price equal to the price passes Google and fails OpenAI. |
| `sale_price_effective_date` | (none in native format) | Convert | OpenAI native wants the current price: "Submit the current price; update the feed when a sale starts or ends." The Google-compatible format keeps the start/end pair. |
| `gtin` | `gtin` | Transfers, check digits | OpenAI: "Exactly 8, 12, 13, or 14 digits, including a valid check digit." Google allows up to 50 numeric characters, so a value Google tolerates can fail here. |
| `mpn` | `mpn` | Transfers | Both preserve punctuation and casing. |
| `brand` | `brand` | Transfers | Both cap at 70 characters. OpenAI adds: "Use a real brand and seller name, not placeholders." |
| `condition` | `condition` | Transfers | Same three values: `new`, `refurbished`, `used`. |
| `product_type` | `product_category` | Rename | Google's `Home > Women > Dresses > Maxi Dresses` is exactly the shape OpenAI wants: "Your category path, from broad to specific, separated by `>`." |
| `google_product_category` | (none) | Drop or fold | OpenAI has no field for Google's taxonomy ID. Keep it in the Google projection only. |
| `shipping` | `shipping_price`, `shipping` | Convert | OpenAI: "Nonnegative amount; zero means no charge. Omitted or empty means unknown, not free shipping." A Google feed that relies on account-level shipping settings sends nothing here. |
| (none) | `seller_name` | Add | Required by OpenAI on every row. Google identifies the seller through the Merchant Center account, not the feed. |
| (none) | `is_eligible_search` | Add or omit | "Omitted or empty: `true`." Set `false` to pull a product out of results. |
| (none) | `is_ads_eligible` | Add | "Set `true` for products Ads should process. Omitted or empty: disabled unless a feed-level Ads default applies." |
| (none) | `is_eligible_checkout` | Add, with policy URLs | "Omitted, empty, or `false`: disabled. Search set to `false` overrides this flag." When `true`, `seller_privacy_policy` and `seller_tos` become required. |
| (none) | `target_countries` | Add | Uppercase ISO 3166-1 alpha-2 codes. "Omitted or empty does not mean worldwide." |
| (none) | `variant_dict` | Add for variants | "Map option names to the selected values." Google has separate `color`, `size`, `material` columns; OpenAI accepts those too and adds the dictionary. |

Three rows in this table deserve a closer look, because they are where a working Google feed turns into silent rejections.

### The availability spelling

The OpenAI specification is explicit about the Google-compatible format: availability "must be `in_stock`, `out_of_stock`, `preorder`, or `backorder`. Use `preorder`, not the OpenAI spelling `pre_order`; `unknown` is not accepted." Read it twice. In the native OpenAI format you write `pre_order`; in the Google-compatible format you write `preorder`. Whichever format you registered, the other spelling is an invalid value for that row. And `unknown`, which the native format allows, is not valid in the Google-compatible one.

### Booleans that Google never had

A Google Shopping feed has almost no boolean columns, so the exporters that build it have no convention for them. OpenAI does: "For boolean fields, use JSON `true` or `false`, or the lowercase strings `true` and `false` in delimited files." A CSV cell containing `TRUE`, `1` or `yes` is a malformed value, and the file upload overview lists "Malformed field values" among the common ingestion failures.

### Defaults that point the wrong way

Two OpenAI defaults are the opposite of what a merchant would guess. An omitted `shipping_price` means unknown, not free. An omitted `target_countries` does not mean worldwide. And an omitted `is_ads_eligible` means the product is not processed for ads unless a feed-level default exists. A Google feed carries none of these columns, so a straight re-upload lands in the least favorable interpretation of each.

## OpenAI's Google-compatible ingestion mode

The specification documents a format that keeps Google's column names so a merchant can reuse an existing export. Its required columns are `id`, `title`, `description`, `link`, `image_link`, `availability`, `price` and `brand`. Variants use `item_group_id`, described as a "Shared parent ID, different from each `id`. Enables grouping." `availability_date` is "Required for `preorder` and `backorder`. Use `YYYY-MM-DD` or ISO 8601 timestamp." `sale_price_effective_date` keeps Google's shape: "start/end with ISO 8601 dates or timestamps."

Two things this mode does not remove. First, the seller identity and eligibility settings still have to exist somewhere; the format has no `seller_name` column, so they live in the feed registration on the OpenAI side rather than in the file. Second, the gate: "Use this format only after OpenAI confirms it for your registered feed." A Google-format file dropped on an SFTP endpoint registered for the native format is a file full of outdated field names, which the file upload overview also lists as a common failure ("Outdated or non-spec field names").

The practical consequence: the Google-compatible mode is a registration decision, not a file decision. Pick it deliberately, or map to the native format.

## What breaks when you just re-upload a Google feed

Assume a merchant exports their Google Shopping CSV, renames nothing, and pushes it to an SFTP endpoint registered for the native OpenAI format. Here is what the specification says will happen, row by row.

| Symptom in the Google export | Effect under the native OpenAI format |
| --- | --- |
| Columns named `id`, `link`, `image_link`, `item_group_id` | `id` and `item_group_id` are accepted as legacy aliases. `link` and `image_link` are not listed as aliases, so `url` and `image_url` are missing: two required fields absent, row rejected. |
| No `seller_name` column | Required field missing on every row. The whole file is rejected, not a few lines. |
| `availability` = `preorder` | Not one of the five native values. Row rejected. |
| `image_link` pointing to a `.webp` file | OpenAI wants a direct JPEG or PNG. Row rejected. |
| `sale_price` equal to `price` during a promotion that ended | Fails "strictly less than `price`". Row rejected. |
| `gtin` with a wrong check digit, tolerated by Google | Fails the check digit rule. Row rejected. |
| Description with `<br>` and `<strong>` tags | Not plain text. Row at risk. |
| No `is_ads_eligible` column | Products are not processed for ads unless a feed-level default applies. The rows are accepted; the ads campaign has nothing to show. |
| No `target_countries` | "Omitted or empty does not mean worldwide." Rows accepted, visibility in your markets not guaranteed. |
| Empty `shipping` because shipping lives in Merchant Center settings | Interpreted as unknown, not free. |

The uncomfortable part is what you see afterwards: nothing. The file upload overview describes how snapshots are processed and how long a record persists, but the specification documents no per-row error report for rejected rows. Products missing from a snapshot are retained "for up to 14 days", then disappear. A merchant who re-uploads a Google feed therefore has two weeks of apparent normality before products that were never accepted stop existing in ChatGPT, and no message in between. The sibling article on [products not showing in ChatGPT shopping](/blog/products-not-showing-chatgpt-shopping/) covers the diagnosis path.

## How to keep one catalog for both channels

The safe pattern is neither "re-upload Google" nor "maintain two feeds by hand". It is one catalog with two projections, each validated against its own specification.

### The native OpenAI projection

Convrail syncs your Shopify or WooCommerce catalog by webhook, debounced for 30 seconds so a bulk edit triggers one rebuild. Each product becomes a native OpenAI row: HTML is stripped from descriptions, the price is formatted as `amount CURRENCY` in the store currency, variants are grouped under a `group_id` distinct from `item_id`, and `seller_name`, `target_countries` and the eligibility flags come from your feed settings rather than from a column you have to invent. A GTIN with a wrong check digit is omitted rather than sent, so the row stays valid instead of being dropped for a typo in a barcode.

Every row is then validated: the nine required fields, the format of each conditional field present, placeholder values such as `n/a` or `unknown` where they are not allowed, `sale_price` against `price`, `is_eligible_checkout` against its policy URLs. Rejected rows never reach the file. They are written to the run journal with the item, the field and the rule, so the check the specification does not give you happens on your side before delivery.

### The Google-compatible projection

The same catalog is also kept as a Google-compatible projection: `id`, `link`, `image_link`, `item_group_id`, `preorder` instead of `pre_order`, no `unknown` availability, `product_type` where OpenAI has `product_category`. Nothing is edited twice. A price change in your store updates both projections on the next rebuild.

### Delivery that matches the overview

The native feed is exported as parquet, `jsonl.gz`, `csv.gz` or `tsv.gz`, sharded at 500,000 items or about 450 MB (under the overview's "Up to 500k items per shard" and "under ~500MB" guidance), with stable names such as `feed-organic-000.jsonl.gz` so each daily SFTP delivery overwrites the previous snapshot as the overview asks: "Keep the same file name on every update and overwrite it with the latest snapshot instead of creating a new name each run." Failed deliveries retry, then open a health alert. If you are choosing between the export formats, the comparison in [parquet vs jsonl vs csv for the OpenAI feed](/blog/parquet-vs-jsonl-vs-csv-openai-feed/) goes into the trade-offs.

## Common mistakes

**Trusting Google's acceptance as proof of validity.** Google is more permissive on GTIN length, image formats and sale prices. A row Google shows for months can fail three OpenAI rules at once.

**Copying `item_id` into `group_id`.** OpenAI wants a "Stable parent-listing ID shared by all variants" that differs from each item identifier. Exporters that fill the group column with the product ID when a product has a single variant produce rows where the two are equal.

**Writing `TRUE` in a boolean column.** Spreadsheet software capitalizes booleans on save. OpenAI wants the lowercase string. Check the file after the last tool that touched it, not before.

**Assuming an empty column means "default to the sensible thing".** For `shipping_price`, `target_countries` and `is_ads_eligible`, the OpenAI default is the restrictive one.

**Adding `utm_medium=feed` to the Google feed but not the OpenAI one.** The best practices page suggests feed attribution parameters on `url` "when you need feed-specific click tracking". If you do it, do it in both projections, and read the [attribution guide](/attribution/) for how those clicks are then matched to orders.

**Starting with the whole catalog.** The overview says to "Start with a small sample (around 100 items)." Fix the mapping on 100 rows, then scale.

## What to do next

Connect your store to [Convrail's product feed](/product-feed/) and read the first run journal: it lists every Google-derived row that the OpenAI specification would reject, with the field and the fix, before anything is delivered. The [OpenAI product feed specification explained](/blog/openai-product-feed-spec-explained/) covers each field in more depth.