commercetools AI Hub product feeds

Description

Exports a selected set of commercetools products (by product selection, category, product type, SKU, product key, store assortment or query predicate) for commercetools AI Hub across vendors (OpenAI Commerce feed, OpenAI Ads feed, Google Merchant Center and Microsoft Merchant Center, including supplemental feeds). Downloads once, then works offline. Maps product data to each vendor spec, checks every vendor's required fields, writes a working file per vendor for the business team to complete, and converts the completed file into the vendor upload format (.jsonl.gz, .tsv, .txt) with upload guidance, including OpenAI manual upload. Use when exporting products for ChatGPT shopping or AI Hub, checking feed readiness or missing required fields, building a supplemental feed, or converting an edited feed file for upload. Not for building or deploying the Connect feed connector itself (see commercetools-connect) and not for loading a catalog into commercetools (see commercetools-catalog-migration).

Installation

Recommended: install the full commercetools plugin. It includes this Skill, every other commercetools Skill, our pre-tuned Subagents, and the commercetools Knowledge MCP — which gives AI live access to the commercetools docs, GraphQL/OpenAPI schemas, and query validation. You only install once; every Skill on this site becomes available in every session.
Install the plugin

In any Claude Code session, run the following commands one at a time:

  1. Add the commercetools AI marketplace:

    /plugin marketplace add commercetools/commercetools-ai-plugins
    
  2. Install the commercetools AI plugin (Skills + Subagents + MCP):

    /plugin install commercetools@commercetools
    
Reload plugins

If you've updated the plugin or installed it in another window and need the current session to pick up the latest version:

/reload-plugins
Claude Desktop
Customize -> Personal plugins -> Create plugin -> Add marketplace -> Add commercetools/commercetools-ai-plugins. Then, click on the plugin and click Install.

Instructions Included

SKILL.md

commercetools AI Hub product feeds

AI Hub feeds go to several vendors: OpenAI Commerce feed (ChatGPT shopping), OpenAI Ads feed, Google Merchant Center and Microsoft Merchant Center. "AI Hub" names the whole export, not one vendor.

Exports what commercetools has for a product selection, per vendor, as a semi-finished working file: every row, every vendor field, blanks where commercetools has no value, and a note per row listing the required fields it is missing. The business team completes the file offline; the completed file comes back and is converted, unchanged, into the vendor upload format. The merchant decides what and when to upload; the skill supplies facts and guidance.
aihub_feeds.py fetch     commercetools          ─► <run>/raw/                 products, categories, Store, manifest
aihub_feeds.py build     raw + mapping + specs  ─► <run>/working/<vendor>-working.csv, report/, NEXT-STEPS.md
   (business team completes the working files offline)
aihub_feeds.py finalize  completed file         ─► <out>/<vendor upload file> + finalize report
scripts/aihub_feeds.py (Python 3.9+, standard library only) is the whole toolchain; run aihub_feeds.py <command> --help for options. The network is used for the download and to read the vendors' current specs; the specs are cached per run, so rebuilding and finalizing work offline.
Run it with python3 scripts/aihub_feeds.py (py -3 on Windows, where python3 is often missing or a Microsoft Store stub). When no Python 3.9+ is found, tell the user to install it from python.org and stop; do not rewrite the script in another language.
Vendor specs are not hardcoded: build and finalize read each vendor's published spec at runtime (fields, required/conditional/optional, allowed values, length limits, Microsoft's supplemental fields) and merge it with the code that maps commercetools data to fields. New spec fields become empty columns, removed ones are dropped, and the report lists both. See references/vendors.md.

Data integrity: commercetools is the only source of product data

Feeds are checked against the storefront, and a guessed value hides a real gap. So:

  • Do not write, rewrite, shorten, translate, summarise or infer product content: titles, descriptions, brands, GTINs, MPNs, categories, colors, sizes, conditions, availability, prices or images. Not in mapping.json, not in the working files, not in the completed file.
  • mapping.json changes only where a value is read from (attribute names, category and Store fields, id strategy) and how an existing value is spelled for a vendor (valueMaps, units, separators). It holds no values of its own.
  • Feed eligibility flags are merchant settings, not product data, and the only defaults: discovery on (is_eligible_search=true), checkout off (is_eligible_checkout=false) and, for the OpenAI Ads feed only, is_ads_eligible=true. A product attribute overrides them; change a default only when the user asks. Google and Microsoft have no checkout flag; their products are listed for discovery by default.
  • Storefront URL, seller name and policy URLs (privacy, terms, refund) come from the Store passed with --store, the Store whose product selections define the assortment.
  • Values the business team adds in the working file are theirs. Convert the completed file as it is: finalize changes the format, never the content. If a value looks wrong, report it; do not correct it.

Before you start

  1. Docs search (required, run first) — The first time you use this skill in a session you must run this before answering. It gathers the latest verified documentation as your primary grounding, filtered to the products this skill covers. Use this script for documentation search while working with this skill; the Knowledge MCP covers everything else. Always confirm details against retrieved documentation rather than the skill text alone:
    node "<this skill's directory>/scripts/docs-search.mjs" \
      --query "<extract key terms: AI Hub, product feed, Product Projections, price selection, Product Selections>" \
      --app-name "<host app: claude-code, claude-chat, cursor, codex, copilot — or the host's own name>" \
      --model "<current-model>" \
      --commercetools-project-key "<if known>" \
      --commercetools-region "<if known>" \
      --limit 10
    
    Pass the commercetools project key and region (as in api.{region}.commercetools.com) only if already in your context; otherwise omit both. Never search files or ask the user for them.
  2. Vendor specs change often, which is why they are read at runtime. When the user asks whether a spec changed, or a vendor flags a field the report marked valid, rebuild the run with --specs live and report its Changes since the previous spec section. If a spec page could not be read, the report says which fallback was used and links the page.

Workflow

  1. Agree the selection and market. Collect, asking only for what is missing:
    • credentials: a dotenv file with the CTP_* variables. Pass it with --env-file and never print or copy secret values;
    • Store, currency, country, distribution channel, locale. aihub_feeds.py fetch --list-stores prints the project's currencies, countries, languages and channels and every Store with its languages, countries, channel keys, product selections and storefront fields;
    • filters: any combination of product selections, categories, product types, SKUs, product keys, attribute conditions (--attribute color=blue), query predicates (--where) and search filters (--search-filter, e.g. inventory or price ranges). --list-categories and --list-attribute map the user's wording to keys. When wording could mean different data (stock as inventory or as an attribute), show both counts and ask;
    • vendors: the user names them; if the request does not say, ask, for example "Which vendors should I build feeds for: OpenAI Commerce feed, OpenAI Ads feed, Google Merchant Center, Microsoft Merchant Center?" Never assume all of them, and do not label one vendor "AI Hub". build requires --vendors (openai, openai-ads, google, microsoft);
    Flags, scopes and filter recipes: references/selecting-products.md.
  2. Resolve, then download. Run aihub_feeds.py fetch --dry-run first. It writes nothing, shows what each name resolved to, counts matching products, samples whether they are in the Store assortment, and checks the feed locale against the data. Show the user the plan and any warnings, then run the download once they confirm. A name that matches several categories stops with the candidates: ask which one is meant (a dry run over the union shows whether the choice changes the result). Ignoring the Store assortment changes the scope, so ask before using --ignore-assortment.
  3. Build the working files. aihub_feeds.py build --run <run> --vendors <named vendors> writes one working file per requested vendor, report/gap-report.md (required fields per vendor first), report/issues.csv and NEXT-STEPS.md. The first build writes <run>/mapping.json.
  4. Review the gaps with the user. For each vendor, report which required fields are filled and which are missing, with example SKUs, and the ⚠? fields whose spec condition may apply (e.g. required for apparel) for the user to judge. Sort gaps into:
    • mapping gaps, where the value exists in commercetools under another name or spelling: point attributes.* (or store.*, categoryFields) at it, or add a valueMaps entry. Confirm with the user, rebuild offline, show before/after counts. See references/mapping-and-files.md;
    • values commercetools does not have: they stay blank in the working file, marked in _missing_required. The team fills them in the file or in commercetools.
  5. Hand over the working files. Point to working/ and NEXT-STEPS.md: how to edit the files safely (import as text so GTINs and SKUs keep their form, keep the header row) and what each helper column means.
  6. Finalize a completed file. When the user hands a completed file back, run aihub_feeds.py finalize --vendor <v> --input <file> --out <dir> (Google and Microsoft: --kind supplemental|primary, optional --columns). When the user wants an upload file straight away, finalize the generated working file as it is. Report the finalize report as facts: rows, required fields still missing or invalid, empty cells in supplemental columns, columns a vendor does not accept. Then give the vendor's upload guidance from its reference. Do not decide for the user whether to upload.

Facts to surface (information, not decisions)

TopicFactWhere
OpenAI Ads feedA separate feed in the ad account (its own SFTP), not the merchant portal feed. Products need is_ads_eligible=true; ads_metadata keys come from the Ads setup and are never invented.vendors.md
OpenAI uploadsEach upload is treated as the complete catalog; products missing from a later upload drop out after up to 14 days.vendors.md
Supplemental idsSupplemental rows apply only to products whose id matches the primary feed. A SKU-vs-key mismatch updates nothing and reports no error.mapping-and-files.md
Supplemental cellsEvery column overrides the primary value, and an empty cell can clear it. --columns limits the file to chosen attributes.vendors.md
Microsoft supplementalAccepts only the fields Microsoft lists for supplemental feeds (read at runtime; id, title, description, size, color, custom_label_0–4, promotion_ID when captured); other fields belong in the primary feed.vendors.md
MarketPrices come from price selection (currency, country, channel); availability from inventory, blank without inventory entries.selecting-products.md

Reference index

TopicReference
Credentials, scopes, --list-stores, filters, store assortment, market context, dry runreferences/selecting-products.md
mapping.json, working files, finalize, reading the gap reportreferences/mapping-and-files.md
Runtime vendor specs, per-vendor facts, file formats, upload steps (OpenAI manual upload, OpenAI Ads feed, Google and Microsoft supplemental feeds)references/vendors.md

Scope

In scope: read-only export of published (or --staged) product data, variant-level rows, price selection, inventory-based availability, Store and seller data, required-field checks per vendor, working files for offline completion, conversion of completed files into vendor upload formats, upload guidance.
Out of scope: uploading files, deciding whether to upload, recurring delivery (a Connect job, see commercetools-connect), shipping and tax settings (configured per account in each merchant center), and changing product data in commercetools.

Checklist

  • The vendors were named by the user (asked when missing, listing OpenAI Commerce feed, OpenAI Ads feed, Google and Microsoft), and only those were built.
  • Flags stayed at their defaults (discovery on, checkout off, ads on only for the OpenAI Ads feed) unless the user asked otherwise.
  • The selection, Store, currency, country and locale were confirmed with the user, and the dry run resolved every product selection, category and product type.
  • The download used an API client with view_products and view_categories (plus view_product_selections / view_stores when used), and no secret was printed.
  • Per vendor, the user knows which required fields are filled and which are missing, with example SKUs. Mapping gaps were fixed in mapping.json with the user's agreement.
  • No product content was written or inferred; mapping.json only points at attributes and fields; the completed file was converted without changing its values.
  • The report says which spec was used per vendor (live with fetch time, or the built-in list when the fetch failed); new or removed spec fields were mentioned.
  • The user has the working files, NEXT-STEPS.md, the finalize report when a completed file was converted, and the upload guidance for each vendor.

References

mapping-and-files.md

Mapping, working files and finalize

Contents

Building

aihub_feeds.py build merges <run>/mapping.json over the default mapping built into the script (objects merge, lists replace) and writes the effective result back to <run>/mapping.json. Edit that file and rebuild.
python3 "<this skill's directory>/scripts/aihub_feeds.py" build --run ai-hub-feeds/<run> \
  --vendors <the vendors the user named> [--mapping other.json] [--specs auto|live|bundled]
--vendors is required and has no default: build only for the vendors the user asked for (openai, google, microsoft).
mapping.json decides where values are read from and how they are spelled for each vendor. It holds no values of its own and no defaults: when no attribute or field matches, the value stays blank and is reported.
Every row is one purchasable variant. The build turns each variant into a vendor-neutral record in <run>/normalized/records.jsonl (keys such as item_id, sku, title, description, url, brand, gtin, price_amount, currency, sale_price_amount, availability, category_path, group_id, variant_options, seller_name), then formats it per vendor. When a vendor value looks wrong, check the normalized record first: it shows whether the cause is the mapping or the vendor formatting.

Item and group ids

KeyDefaultUse
itemIdskusku, variantKey, productKeyVariantId or template
itemIdTemplate{productKey}-{variantId}Used when itemId is template
groupIdproductKeyproductKey, productId or slug; set only on products with more than one variant
For Google and Microsoft supplemental feeds, itemId must reproduce the id of the merchant's existing primary feed. Ask the user for two or three ids from their merchant center and compare them with normalized/records.jsonl before handing over the files. A wrong id strategy updates nothing, and neither merchant center reports it as an error.

The report warns on duplicate ids, which usually means variants without SKUs.

URLs and seller data

These are read from the Store passed with --store. Each store.<key> is an ordered list of paths into the Store; the first non-empty one wins. custom.<name> reads a Store custom field, and a trailing @origin keeps only the scheme and host of a URL.
KeyDefault pathsFeeds
store.baseUrlcustom.sellerStoreUrl, storefront.orderUrlTemplate@origin, storefront.checkoutUrlTemplate@origin{baseUrl} in product URLs; OpenAI seller_url
store.sellerNamecustom.sellerName, nameOpenAI seller_name (required)
store.privacyPolicyUrl, store.termsOfServiceUrlstorefront.privacyPolicyUrl / storefront.termsOfServiceUrl, then custom fieldsOpenAI, required when checkout is enabled
store.returnPolicyUrlstorefront.refundPolicyUrl, custom.returnPolicyUrlOpenAI return_policy
store.returnWindowDays, store.acceptsReturnscustom.returnWindowDays, custom.acceptsReturnsOpenAI; the window is only sent when returns are accepted
Add paths when the project keeps these values elsewhere, for example "store": { "baseUrl": ["custom.shopUrl"] }.
productUrlTemplate ({baseUrl}/products/{slug}) sets the shape of product page URLs. Add ?sku={sku} for variant-specific landing pages, which Google prefers. Placeholders: {baseUrl} {slug} {sku} {variantId} {variantKey} {productKey} {productId} {locale}. When {baseUrl} is used and the Store has no storefront URL, product URLs stay empty.

Attributes

attributes.<field> is an ordered list of commercetools attribute names. Matching ignores case, variant attributes win over product-level attributes, and the first non-empty value is used. Values are rendered as follows: enum and lenum use the label, localized values the feed locale, money becomes amount CUR, and references the expanded name or key.
FieldDefault namesFeeds
brandbrand, manufacturer, designerall
gtin, mpngtin, ean, upc, barcode / mpn, manufacturerPartNumberall; GTINs are checked for length and check digit
color, size, sizeSystem, material, patternsame namesall
gender, ageGroup, conditionsame namesall, after valueMaps
weight, length, width, height (+ weightUnit, dimensionsUnit)same namesOpenAI, Google
isEligibleSearch, isEligibleCheckoutchatgptEnableSearch, chatgptEnableCheckoutOpenAI
reviewCount, starRatingreviewCount / starRating, averageRating, ratingOpenAI
googleProductCategorygoogleProductCategoryGoogle, Microsoft (wins over categoryFields)
identifierExistsidentifierExistsGoogle identifier_exists, Microsoft identifier_exists; never derived from a missing GTIN
promotionId, customLabel0–customLabel4promotionId / noneGoogle, Microsoft
The report section commercetools attributes not used by any feed field lists attribute names in the data that no field reads. It is the fastest way to find a brand or barcode stored under a project-specific name.
Measure attributes accept numbers, strings such as "250 g" or "20 cm", and { "value": 1.2, "unit": "kg" }. A bare number needs a unit from a weightUnit / dimensionsUnit attribute. Without one the measure stays blank and is noted in the report.

Feed flags

flags holds the merchant's eligibility settings, used when a product has no attribute for them: isEligibleSearch (true, discovery), isEligibleCheckout (false) and isAdsEligible (true, written only in the OpenAI Ads feed). Attributes in attributes.isEligibleSearch / isEligibleCheckout / isAdsEligible override per product. The report header lists the defaults and how many rows used them. Change a default only on the user's request.
adsMetadata maps OpenAI Ads metadata keys to attribute names, e.g. {"custom_label_0": ["season"]}; use only keys the Ads integration is configured for.

Value maps and availability

  • valueMaps.gender|ageGroup|condition: re-spells existing project values (case-insensitive) as spec values, for example "herren": "male". Only map values that mean the same thing. Values that are still outside the spec stay in the working file as they are and are flagged in _issues.
  • There is no default condition. Vendors treat an empty condition as new; products sold used or refurbished need a condition attribute in commercetools.
  • Availability comes only from inventory. restockableAsBackorder (false) sends out-of-stock variants with restockableInDays as backorder; onlyInStock (false) drops out-of-stock variants.

Variants and titles

variantOptions lists the attributes that can distinguish variants, with display labels (default color, size, material, pattern). Only options whose values differ between a product's variants are used, and they become OpenAI variant_dict.
Titles are sent exactly as the product name in commercetools. appendVariantToTitle (false) appends the varying option values, such as Classic Tee - Red / M. It only reuses commercetools values, but it changes the title, so turn it on only when the user asks. Titles over a vendor's length limit are flagged, never shortened.

Categories

product_category (OpenAI) and product_type (Google, Microsoft) use one category path per product, for example Women > Clothing > Dresses. With categoryPath.preferFilteredCategories (default true) a category inside the downloaded --category tree is preferred; categories under the roots listed in categoryPath.excludeRoots (root category keys or names, e.g. ["new", "sale"] for merchandising trees) come last. The report notes when every path starts with the filtered category, the usual sign that it is a merchandising tree. Otherwise the commercetools order applies. The Google product taxonomy value comes from the product attribute in attributes.googleProductCategory, otherwise from the category custom fields listed in categoryFields.googleProductCategory. The most specific category wins, walking from the assigned category up to its root, so a value on a top-level category covers its whole tree.
{
  "productUrlTemplate": "{baseUrl}/{locale}/p/{slug}?sku={sku}",
  "attributes": { "brand": ["brandName"], "gtin": ["ean13"], "customLabel0": ["season"] },
  "categoryFields": { "googleProductCategory": ["googleCategoryId"] },
  "store": { "baseUrl": ["custom.shopUrl", "storefront.orderUrlTemplate@origin"] },
  "categoryPath": { "excludeRoots": ["new", "sale"] }
}

Working files

working/<vendor>-working.csv (UTF-8 with BOM, comma-separated) has one row per variant and one column per vendor field, in spec order with the id first, including fields commercetools has no values for, so the team can fill them. Two helper columns lead:
ColumnContent
_missing_requiredRequired fields this row has no valid value for
_issuesOther findings: invalid optional values, over-long text, recommended fields missing
Values are vendor-formatted (79.99 USD, in_stock, in stock for Microsoft), flagged values are kept as they are, and lists use commas (%2C inside URLs). OpenAI object fields (variant_dict, dimensions) are JSON text.
Guidance for the team, also in NEXT-STEPS.md: import as text so GTINs, SKUs and prices keep their exact form (spreadsheets otherwise turn 00012345600012 into 1.23456E+10), keep the header row, and save as CSV UTF-8. Comma-, semicolon- and tab-separated files are all read back.

Finalizing a completed file

python3 "<this skill's directory>/scripts/aihub_feeds.py" finalize --vendor openai \
  --input <completed>.csv --out <dir> [--format jsonl.gz|csv.gz|tsv.gz]
python3 "<this skill's directory>/scripts/aihub_feeds.py" finalize --vendor google|microsoft \
  --input <completed>.csv --out <dir> [--kind supplemental|primary] [--columns brand,gtin,...]
finalize reads the run's cached specs (<run>/specs, next to working/; --specs-dir otherwise), drops the _ helper columns and columns that are not vendor fields, writes every row with its values exactly as given, and converts only the format:
VendorOutput
OpenAI Commerce feedproduct-feed.jsonl.gz (typed JSON: booleans, numbers, arrays, objects), or .csv.gz / .tsv.gz
OpenAI Ads feedads-product-feed.csv (UTF-8 CSV), or `--format jsonl.gz
Googlegoogle-supplemental-feed.tsv (id first), or google-primary-feed.tsv with --kind primary
Microsoftmicrosoft_supplemental_feed.txt (only the fields Microsoft accepts there, id last) or microsoft_primary_feed.txt with --kind primary
For supplemental feeds, the default columns are all columns with at least one value; --columns restricts them. It then re-runs the vendor checks and writes <vendor>-<kind>-finalize-report.md: rows with every required field, required fields still missing or invalid, empty cells per supplemental column, duplicate or missing ids, and columns that were not written (not accepted by the vendor, not requested, or empty in every row). Mention columns that were empty in every row, e.g. no descriptions to send. Report these as facts; the decision to upload is the user's.

Reading the gap report

The report starts with a summary per vendor: rows, rows with every required field, rows missing at least one. Each vendor section then has:

  • Required fields: every always-required and conditionally required field with filled / missing / invalid counts; ✓ when complete, ⚠ when a required value is missing or invalid, ⚠? when the field is empty and the spec's condition (shown in When) may apply;
  • All fields: Filled, Flagged (sent with a note or re-spelled for the vendor, such as an over-long title or backorder as out of stock for Microsoft), Invalid (kept, but outside the spec) and Missing, plus the commercetools source of each field;
  • Top issues with example ids, and where each missing value would come from.
For Google and Microsoft the required fields are those of the full product spec; a supplemental feed itself only needs id. report/issues.csv has one line per row and field.
selecting-products.md

Selecting products

aihub_feeds.py fetch is the only command that calls commercetools. It is read-only and writes everything later steps need into <run>/raw/, so the build can be repeated offline.

Credentials and scopes

The script reads the same variables commercetools Connect applications use, from the environment or from a dotenv file passed with --env-file:
VariableRequiredNotes
CTP_PROJECT_KEY, CTP_CLIENT_ID, CTP_CLIENT_SECRETyesAPI client credentials.
CTP_AUTH_URL, CTP_API_URLyeshttps://auth.{region}.commercetools.com, https://api.{region}.commercetools.com.
CTP_SCOPEnoSpace-separated; :{projectKey} is appended when missing. Omit to get all scopes of the client.
CTP_STORE_KEY, CTP_CURRENCY, CTP_COUNTRY, CTP_LOCALE, CTP_DISTRIBUTION_CHANNEL_KEYnoDefaults for --store, --currency, --country, the feed locale and --channel.
Use a read-only API client. The scopes it needs (scopes):
  • view_products: product projections, product types, channels;
  • view_categories: category paths and --category filters. Without it the download still runs, but category filters fail and product_category / product_type stay empty;
  • view_product_selections: with --product-selection, and with --store when the Store has product selections;
  • view_stores: only with --store.

Pass the dotenv file by path. Do not read it into the conversation or echo its values.

Finding the market context

python3 "<this skill's directory>/scripts/aihub_feeds.py" fetch --env-file <path to .env> --list-stores
prints the project's currencies, countries and languages, every channel with its roles, every Store with its languages, countries, distribution and supply channel keys, active product selections and which storefront / custom fields it has, and every product selection with its mode and product count. Two more listings map the user's wording to keys:
  • --list-categories [TEXT]: key: path for every category whose path contains TEXT, e.g. --list-categories "women > shoes";
  • --list-attribute NAME: the attribute's type and, for enums, key: label pairs, e.g. --list-attribute color. Use it to propose --store, --currency, --country, --channel and --locale instead of guessing, and confirm them with the user.

Market context

These decide which price and stock a row carries, so confirm them before downloading.

  • --currency, --country, --channel (distribution channel key), --customer-group turn on price selection. Each variant then carries the one best-matching price, including Standalone Prices, and its discounted value. Without --currency the build picks a price offline from embedded prices and flags it in the report.
  • --store adds storeProjection, which limits locales, prices and inventory to the Store's configuration (Product Projections), and applies the Store's active Product Selections: Individual selections include only assigned products, IndividualExclusion selections remove excluded ones, and variant selections (includeOnly, includeAllExcept) limit SKUs. Use --ignore-assortment to export products outside the Store's assortment. That changes the scope of the export, so it is the user's call: when the dry run reports that the selection and the assortment do not overlap, ask. The same Store supplies the storefront URL used in product links, the seller name and the privacy, terms and refund policy URLs. Without --store these fields stay empty. A Microsoft supplemental feed carries none of them, so a Store matters there only for prices, inventory scope and assortment.
  • The feed locale (--locale, else CTP_LOCALE, else the Store's first language, else en-US) is used for names, descriptions, slugs and localized attribute values. A locale falls back to its language (en-US reads en) and to other locales of that language. When a product has no text in that language the value stays blank and the row is listed under Normalization notes. allowLocaleFallback: true in mapping.json uses another language instead; only set it when the user accepts text in another language in the feed.
  • Availability comes from inventory on the variant. With several supply channels a variant is in stock when any channel is. Variants without inventory entries get no availability and are reported.

Filters

All filters are combined with AND. Category, product type, SKU and product-key values may be repeated or comma-separated.

NeedFlags
One category tree--category mens (id, key, slug or name; sub-categories included, --no-subtree to disable). A name or slug used by several categories, such as "Women" under both Women and Sale, stops with the candidate keys and paths: show them to the user and re-run with the chosen key
Several categories--category shirts --category "Kitchen Knives"
Product type--product-type apparel (key, id or name)
Specific variants--sku TEE-RED-M,TEE-RED-L (only those variants become rows)
Specific products--product-key classic-tee
One product selection--product-selection "Summer picks" (key, id or name; only the products, and variants, it includes)
Store assortment--store us-store (all of the Store's active product selections)
Attribute value (any type)--attribute color=blue, --attribute designer=michaelkors,karllagerfeld, --attribute size>=40, --attribute isOnStock=true
In stock (inventory in any supply channel)--search-filter "variants.availability.isOnStockInChannels:exists"
In stock in one channel--search-filter 'variants.availability.isOnStockInChannels:"<channel id>"'
In stock (inventory without supply channel)--search-filter "variants.availability.isOnStock:true"
Price below 200 (selected price)--currency USD --country US --search-filter "variants.scopedPrice.currentValue.centAmount:range(* to 19999)" (range ends are inclusive; currentValue is after Product Discounts, value before, so ask which should count)
Any embedded price below 200--where 'masterVariant(prices(value(currencyCode="USD" and centAmount < 20000)))' (any matching Embedded Price, not necessarily the one the feed carries)
Recently changed--where 'lastModifiedAt > "2026-10-01T00:00:00.000Z"'
Sample--max-products 100
Draft data--staged
--product-selection and --store can be combined: a product must be in both. Use --store as well when the export should carry the Store's storefront URL, seller and policy data, or its price and inventory scope.

Three ways to filter

MechanismEndpointUse forVariant handling
--attributeProduct Projections queryAny product or variant attribute, by name, typed from the product typesOnly matching variants (--attribute-scope product keeps whole products)
--whereProduct Projections queryAnything query predicates can express: product fields, embedded prices, dates, keys, nested attributesWhole products
--search-filterProduct Projection SearchWhat predicates cannot: inventory (variants.availability.isOnStock, availableQuantity), selected prices (variants.scopedPrice…), attribute facets and rangesOnly matching variants (--all-variants keeps all)
--where and --search-filter use different endpoints and cannot be combined; categories, product types, SKUs, product keys, product selections and --attribute work with both (attribute conditions are sent as predicates or search filters, then checked per variant). All filters are ANDed.
Price filters only see Embedded Prices; Standalone Prices cannot be filtered with --where or --search-filter. Use the scoped-price filter when the cut-off should match the price the feed carries, and --where when any price in a currency is enough.
Product Projection Search is deprecated and cannot be activated for Projects created after 31 August 2026. There, --search-filter fails: use --where or --attribute, and for stock read the availability column of the working file instead of filtering.
"In stock" can mean commercetools inventory (search filters above; also what the feed's availability is built from) or a project attribute such as isOnStock. When both exist, they usually give very different results: show the user both counts and ask.
Several --attribute conditions must hold on the same variant (one combined predicate). Conditions the query cannot express (!=, localized text, money) are checked per variant only; the dry run then warns that its match count still includes products they will drop.
--attribute name<op>value: operators =, !=, >, >=, <, <=; = and != take a comma-separated list (color=blue,black). The attribute type comes from the product types: enum and lenum values match by key or by label in any locale (gender=Herren means key men), numbers and dates compare as such, set attributes match when any element matches. An unknown attribute name or enum value stops with the valid names or keys, so a dry run is the way to map the user's wording ("blue shirts by a given designer") to attribute names and keys; it prints the resolved conditions. Confirm the mapping with the user when the wording could mean more than one attribute (for example stock as an isOnStock attribute versus inventory).
Search filter syntax: variants.attributes.<name>:"value" (text), variants.attributes.<name>.key:"value" (enum), range(<from> to <to>) with * for open ends, money in cent amounts, and scoped-price filters only after --currency (and --country) select a price.
Pagination uses an id cursor (sort=id asc, id > "<last>"), so there is no offset limit on large catalogs.

Running it

python3 "<this skill's directory>/scripts/aihub_feeds.py" fetch \
  --out ai-hub-feeds/<run> \
  --env-file <path to .env> \
  --store <store-key> --currency USD --country US --channel <channel-key> \
  --category <category> --dry-run
--dry-run writes nothing and needs no --out. It prints a short summary (resolved names, search filters, number of predicates, products matching the query before the Store assortment and per-variant checks, variants kept in a sample, warnings); add --json for the full plan with the query. It resolves product selections, categories, product types and attribute conditions and prints, in addition to the query (endpoint, predicates or search filters), sampleVariantsKept when variant filters apply, and: what each name resolved to (resolvedCategories as key: path, resolvedProductSelections), how many products match the query (productsMatchingQuery), how many sampled products are in the Store assortment / product selection (sampleInAssortment), and whether the feed locale matches the sampled product names (localeCheck). It stops with a message for unknown product selections, categories or product types: ask the user which one they meant rather than guessing. Remove --dry-run to download.
Output in <run>/raw/:
FileContent
products.jsonlOne Product Projection per line, with _feed.selectedSkus when only some variants are in scope
categories.jsonAll categories, for category paths and category mapping
store.jsonThe Store: name, storefront URLs and policy URLs, custom fields, product selections
fetch-manifest.jsonResolved filters, query, counts, warnings. No credentials
Audit metadata (createdBy, lastModifiedBy, which carry the API client id) is removed before anything is written. The run folder still contains catalog data. Keep it out of version control.
vendors.md

Vendors: specs, files and uploading

Contents

Uploading is the merchant's step; the upload sections below are guidance for them. If the user asks the agent to upload or publish (SFTP, a hosted URL, a Merchant Center data source), first show the target, the file and its effect (an OpenAI Commerce upload replaces the whole catalog, a supplemental feed overrides primary values, an Ads feed can put products into live campaigns), and act only after the user's explicit yes.

Specs are read at runtime

Which fields exist, which are required, their allowed values and length limits change without notice, so the skill does not hardcode them. aihub_feeds.py build (and finalize) fetch the vendor's published spec, parse it and cache it in <run>/specs/<vendor>.json; later offline builds of the same run use the cache. aihub_feeds.py specs --vendors <v> shows the current spec on its own.
VendorSpec read from
OpenAI Commerce feed, OpenAI Ads feeddevelopers.openai.com/commerce/specs/file-upload/products (Markdown version, .md); Ads rules from the OpenAI Ads product feeds guide
Googlesupport.google.com/merchants/answer/7052112 (attribute tables)
MicrosoftMicrosoft product attributes and Microsoft feed files and supplemental feeds (fields a supplemental feed accepts)

What stays in code is how commercetools data becomes each field (the field registry). The gap report compares both and lists:

  • fields in the spec with no commercetools mapping: written as empty working-file columns the team can fill; tell the user, and add a mapping when the data exists in commercetools;
  • fields no longer in the spec: left out;
  • requirement changes, e.g. a field that became required.
--specs live refetches; the previous copy is kept as <vendor>.previous.json and the report opens the vendor section with Changes since the previous spec of this run (fields added, removed, requirement / allowed values / length changed). Use this when the user asks whether a spec changed. --specs bundled uses the registry's own list (captured 2026-10-23).

When a spec cannot be fetched or no longer parses (page layout changed), the run's cached spec is used, else the registry's list; the console and the report say which. Tell the user and link the spec page.

Conditions in the spec that the data cannot answer ("required for apparel in the US", "for new products") are shown as ⚠? in the required-fields table with the rows where the field is empty: the merchant decides whether the condition applies.

OpenAI Commerce feed

Working file working/openai-working.csv; finalize --vendor openai writes product-feed.jsonl.gz (or --format csv.gz|tsv.gz), every row, values as given. OpenAI accepts gzipped CSV, TSV and JSONL (Parquet with zstd is preferred for large automated feeds), UTF-8, up to about 500k items and ~500 MB per file.
Built from commercetools: SKU → item_id; product name → title; description (HTML stripped) → description; productUrlTemplate + Store storefront URL → url; attributes → brand, gtin, mpn, color, size, gender, age_group, condition, measures; first / other images → image_url / additional_image_urls; price selection → price, sale_price; inventory → availability; product key → group_id with listing_has_variations and variant_dict (varying options) on multi-variant products; Store → seller_name, seller_url, policy URLs; chatgptEnableSearch/Checkout attributes → is_eligible_search / is_eligible_checkout.
Flags: the working file carries is_eligible_search=true (discovery) and is_eligible_checkout=false from the mapping's flags defaults unless a product attribute says otherwise. The privacy policy and terms URLs only matter once checkout is true, which also needs the checkout integration enabled with OpenAI. is_ads_eligible is not part of the merchant feed; use the OpenAI Ads feed export for ads. If offer_id is added, it must not contain the price.

Full snapshot semantics

Every upload is the complete catalog. A product missing from a later upload is kept for up to 14 days and then removed, so an upload of a filtered selection becomes the catalog ChatGPT shows. Tell the user this when the export is a subset; whether to upload is their call. To hide a product without removing it, send is_eligible_search=false. OpenAI suggests validating about 100 items first and refreshing at least daily.

Uploading (merchant portal → product feed)

Manual upload, on the product feed page of the OpenAI merchant portal:

  1. Manual upload → Upload a file ("Upload a compressed feed file from your device") → Choose File → the finalized product-feed.jsonl.gz.
  2. Validate. Problems go back into the working file (or the mapping / commercetools), then finalize and validate again.
  3. Submit feed.
For recurring delivery: SFTP (username/password, shown once after Generate password, or SSH key; upload to the root and overwrite the same file names, all shards without long pauses) or Hosted URL (public HTTPS URL, run frequency Once / Hourly / Daily). Recurring SFTP delivery from commercetools is a Connect job (see commercetools-connect).

OpenAI Ads feed

A product feed in the OpenAI ad account, used by product-feed campaigns. It is separate from the merchant portal feed: created in the ad account (needs product feed API access), with its own SFTP access (password or SSH key), and processed for ads rather than ChatGPT discovery.

Working file working/openai-ads-working.csv; finalize --vendor openai-ads writes ads-product-feed.csv (UTF-8 CSV, as the Ads guide asks; --format for others). It is the OpenAI product schema plus:
FieldFromNote
is_ads_eligibleattribute chatgptEnableAds / isAdsEligible, else flags.isAdsEligible (true)Required here: products Ads should process
ads_metadataadsMetadata in mapping.json, key → attribute namesKeys must be the ones configured for the Ads integration (e.g. custom_label_0); never invent keys
target_countries, store_countrythe download's --countryThe market of the export
Discovery and checkout flags keep their defaults (true / false), as in the Ads guide's example. Uploading: put the CSV in the ad feed's SFTP root, check upload history and diagnostics, then select products for ad groups with product sets (brand, category, price, ads_metadata.<key>). Prices and availability must stay current; product pages and images must be public HTTPS URLs.

Google Merchant Center

Working file working/google-working.csv; finalize --vendor google writes google-supplemental-feed.tsv (default) or google-primary-feed.tsv (--kind primary), tab-separated, header row, id first.
A supplemental data source adds or overrides attributes of products that already exist in a primary source, matched by id; it cannot add or remove products. So:
  • id must equal the primary feed id exactly. Compare a few ids from Merchant Center with the working file before finalizing (item ids).
  • Rows missing required fields of the full spec still belong in a supplemental file.

Choosing columns

Every column overrides the primary value, and an empty cell can clear it. Ask which attributes the supplemental feed should own and pass them with --columns, e.g. --columns gtin,brand,google_product_category,custom_label_0; otherwise every column with a value is written. The finalize report lists empty cells per column.
Shipping, tax and returns are usually configured per account in Merchant Center, not in the feed. google_product_category comes from an attribute or category custom field, never chosen by the agent.

Uploading a supplemental data source

  1. Merchant Center → Settings → Data sources → Supplemental sources → Add supplemental product data (some accounts need the Advanced data source management add-on).
  2. Upload a file (or Google Sheets / scheduled fetch for recurring updates).
  3. Select the primary data source(s) it applies to, save, then review Products → Needs attention.

Microsoft Merchant Center

Working file working/microsoft-working.csv with every primary-feed field; finalize --vendor microsoft writes microsoft_supplemental_feed.txt (default) or microsoft_primary_feed.txt (--kind primary). File rules applied: tab-delimited .txt, one header row, no spaces and at most 50 characters in the file name, no tabs or line breaks in values, no HTML, id last (Microsoft rejects lines ending in a tab).

Supplemental feed limits

A supplemental feed overrides fields of products in a primary feed with the same Feed label (or Country of sale), Language and id, and only accepts the fields Microsoft lists for supplemental feeds (read at runtime; at capture time id, title, description, size, color, custom_label_0–4, promotion_ID). Finalize writes only those and lists the rest as not written. Gaps in other fields (GTIN, brand, price, availability, images) need the primary feed: the merchant's own, or one finalized with --kind primary. Say this when the user expects a supplemental feed to fix identifiers or prices. A Store matters for a Microsoft supplemental export only through prices, inventory scope and assortment.

Uploading a supplemental feed

  1. Microsoft Advertising → Tools → Merchant Center → Feeds → store → Create feed.
  2. Feed type: Supplemental feed (or Primary feed for --kind primary); same Feed label / Country of sale and Language as the primary feed.
  3. Input method: Manually upload (< 4 MB) or FTP/SFTP (< 1 GB; file name must match the feed settings) → Create feed. Feeds expire after 30 days.