The
/sample, /test, and public-feed reads need no key at all. Only the live import
(ingestProducts) needs sk + HMAC. See
Key Usage by Endpoint.1. A sample feed to model yours after
A real, valid sample feed carries the exact field names and covers both a single-variant product and a multi-variant one, whose variants share aproduct_group. No key required.
Response
2. Dry-run validation, including a bad row
The validator parses every row and returns the samesummary and errors a real import would,
plus a normalized_preview of how Galactic Core interpreted each row — while writing nothing. It
needs no key, so the format can be settled before any request is signed.
A feed carrying one valid row and one invalid row (a negative price) shows the contract in full: the
validator accepts the good row, rejects the bad one, and status becomes partial:
Response
status—valid(no failures),partial(some rows rejected), orinvalid(all rejected).summary— the exact counts a real import would produce:rows_total/rows_created/rows_updated/rows_skipped/rows_failed.errors[]— one entry per rejected row, naming the 1-basedrow, the offendingsku, thefieldthat failed, and a readablereason, hereprice must be a positive number.normalized_preview[]— every row as Galactic Core interpreted it, withcategoryandbrandresolved and anything it could not infer leftnull, which is where the mapping is confirmed before signing.
status: valid, rows_failed: 0, and an empty errors array.
Flagged rows can be fixed and re-run against the validator as many times as needed, since the live
request is signed only once the counts are right.
3. Pushing the catalog (secret key + HMAC)
The real write is upsert-by-SKU: an existingsku is updated, a new one created. Rows are applied
independently, so one bad row never fails the batch, and the response carries created / updated /
skipped / failed counts plus an errors array for any rejections.
Three signing headers are required, on the same scheme as orders and payments: an Idempotency-Key,
where retrying with the same key returns the original result rather than re-importing; an
X-Timestamp in unix seconds, within 5 minutes of server time; and an X-Signature, the base64
HMAC-SHA256 of `${timestamp}.${body}` signed with the store’s signing secret from Integrations →
API keys.
rows_created: 2, errors: []:
Response
rows_updated: 2, nothing
duplicated, which is what makes the import safe to run on a schedule. A batch mixing good and bad
rows returns status: partial with the per-row errors array, as the dry run did, and the good rows
are still written.
Four rules govern a first import:
- A category must already exist. Categories are a fixed platform-level set, so a row whose
categorydoes not match one of the store’s enabled categories is rejected, never auto-created. The name must match exactly. - A subcategory is auto-created. Subcategories are the merchant’s to define freely, and an
unknown one is created on the fly.
>nests them, as incategory: "Electronics",subcategory: "Audio > Headphones". - Variants group by
product_group. Rows sharing aproduct_groupbecome variants of one product, each still carrying its own uniquesku. - CSV and XML behave identically.
format: 'csv'(or'xml') with a string body follows the same upsert and partial-success rules. A CSV body is the header row plus data rows as one string:
gtin, mpn, condition — along with weight_grams and
dimensions_cm, the google_product_category and meta_product_category taxonomy strings, tags,
the seo_title / seo_description / seo_keywords metadata, and is_online_enabled with
online_price and online_sale_price. Column names are read leniently, so weight_kg converts to
grams and a comma-delimited tags string becomes a list. See
Catalog identity and channel fields.
Carrying your own columns
A feed is not limited to the fields above. A column that matches the label of a custom field defined on products for the store is recorded on the product; anything else is ignored, so extra columns are safe to leave in.Fields the sync leaves alone
A scheduled sync runs against a catalog the merchant also edits, so each source records which side owns each field. Ownership is a property of the registered source and governs scheduled sync alone; a push applies the values in the batch as sent. The feed owns what moves in the source system —price, cost_price, stock, threshold, and the barcode / gtin / mpn identifiers — and
rewrites those on every run. The
merchant owns the listing: name, description, category, subcategory, brand, image, the
SEO and channel fields, and their own columns. A merchant-owned field is refreshed only while it
still matches what the feed last wrote, so an edit made in admin stands rather than being reverted on
the next run.
That is worth knowing when a feed appears to stop updating a product: a name or category that no
longer tracks the source has usually been edited on the store’s side, and the sync is leaving it
alone by design. Either side can be reassigned per source and per field.
Which side owns a field covers the full split.
4. Publishing the catalog back out
The inverse direction. Opting in under admin → Integrations → Catalog sync → “Publish my catalog” makes the catalog readable at a stable public URL with no key required, so any system can pull it — another store’s scheduled sync, a partner integration, a script. Only storefront-safe fields are exposed, never cost or margin. The SDK call below abstracts the host away; the dashboard shows the full URL to copy, and where the store belongs to an operated platform that URL carries the platform’s own host — see Publish your own catalog.Response
subcategory, brand, and sale_price are nullable: the first product has subcategory: null
and a sale_price of 129, being on sale, while the second has sale_price: null and is not. The
feed shape matches what the ingestion endpoints accept, which makes a store-to-store sync a direct
pull-and-ingest with no field mapping. server.feeds.getStoreCatalogFeedXml({ store }) returns
the same data as an XML document. A feed protected with a token takes it as
{ store, token: 'YOUR_FEED_TOKEN' }; a wrong or missing token, or a store that has not opted in,
returns 404.What’s next
feed.sync.completedin Webhooks & automation fires the moment an inbound feed-pull finishes.- With products in, Search & discovery and the Storefront checkout come next.

